mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
Compare commits
498 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eca5f89e59 | |||
| 0b0beac122 | |||
| 578e97123d | |||
| 01d672d563 | |||
| 490b2d66e5 | |||
| 0368c4846f | |||
| 4dfc02c5da | |||
| 5c84b0c6d3 | |||
| b6c06e8c30 | |||
| c6435d5606 | |||
| 2887d212e3 | |||
| cadae52d5d | |||
| d9749ca65b | |||
| c2070cd99d | |||
| 03d34e9a10 | |||
| 45e76a6df6 | |||
| 28dd3c2a03 | |||
| 5de981d923 | |||
| a55b31e7c3 | |||
| 5f0bd8180e | |||
| e1f1fcccbe | |||
| ab04aef561 | |||
| 79ddb1f58e | |||
| c7a2e68941 | |||
| 5c592928d4 | |||
| bcab4224fb | |||
| 892f70b5b5 | |||
| 5c7de5ad75 | |||
| 995088b522 | |||
| ef503646ee | |||
| a760470e48 | |||
| 31d1b1b91d | |||
| 7524c80af6 | |||
| 721d5134b7 | |||
| 0b5321fdd7 | |||
| c86058fed1 | |||
| 8b295b5e9d | |||
| 221248e691 | |||
| 7621247bb7 | |||
| 463a50ebd4 | |||
| 62aabc6ae1 | |||
| 1ca9aa5ca6 | |||
| 7a76f8dce2 | |||
| beca27599c | |||
| 256b167eaf | |||
| 5526d4c24d | |||
| 7ea138c640 | |||
| c4a659c3b5 | |||
| 0a3991c314 | |||
| d1a9e0bbe3 | |||
| 17eacfdf95 | |||
| 9ec0762d41 | |||
| 30f39f1850 | |||
| 8ee07330b3 | |||
| cffde4564d | |||
| ce7a7649a2 | |||
| eeed7e6a0a | |||
| d6844f5239 | |||
| 06c4019e81 | |||
| 7785a8cc58 | |||
| 663516c1e3 | |||
| e83667a20b | |||
| aa96d75fb9 | |||
| a5af4bc5ed | |||
| 57161ba5ad | |||
| b1a9254fc1 | |||
| e56e818659 | |||
| 79de0d5875 | |||
| f6f8140ebc | |||
| eed221af46 | |||
| 524457a4e6 | |||
| 8e289b7a7d | |||
| 172b69cf15 | |||
| 475279a4fa | |||
| 4b236bf9ff | |||
| 50e2d10029 | |||
| fa8a57f1ab | |||
| 715f8b420b | |||
| a0637d86ff | |||
| 1a941d7f92 | |||
| b5959aa3fa | |||
| 8861dd2401 | |||
| da73f4b395 | |||
| d5097d0fe5 | |||
| 022ad4be0d | |||
| 2d2a83e9e8 | |||
| 67c7bee4fa | |||
| 675b0f1ec8 | |||
| c2b86c3ab3 | |||
| 46337b8085 | |||
| df172d8eed | |||
| 24b9160b79 | |||
| a7860b0b8e | |||
| 7471c224fa | |||
| 2996135155 | |||
| 1abab8d440 | |||
| e52c83e5be | |||
| 5ed4545737 | |||
| 8b39df68ce | |||
| 04d8a3762b | |||
| 0f7c8d00d6 | |||
| 12fb4f8639 | |||
| 6a84b825e6 | |||
| e8595c56b3 | |||
| 4a9fb2fa74 | |||
| d8fae6d689 | |||
| 252f9a0e46 | |||
| 8d24b4a217 | |||
| 43920f79a9 | |||
| 55bfc5856b | |||
| cd9d4acbc2 | |||
| 2b5355419a | |||
| 22b822ce87 | |||
| b2c298b926 | |||
| 671f3df115 | |||
| 2dd77001b7 | |||
| 5f574ded81 | |||
| 6309e6818d | |||
| 4813ab6ffb | |||
| 4ebba1298a | |||
| 2e0d1bb5a0 | |||
| 09d2b16767 | |||
| f56556eb73 | |||
| f387ca8624 | |||
| ca9d783cf9 | |||
| 863ddea50b | |||
| 00b4bb6305 | |||
| d217503a6a | |||
| b6012862c4 | |||
| 60c0ad006f | |||
| 79ff332afe | |||
| 595bbe32a4 | |||
| 328427bfdb | |||
| 51f556799c | |||
| 5e0a0855ea | |||
| 7ec3460d73 | |||
| 4c8c42cd20 | |||
| 09682dd393 | |||
| 82c18d3848 | |||
| 3bdba9210d | |||
| 371620d161 | |||
| d10281f851 | |||
| 47ceb2419b | |||
| 771b5b2e53 | |||
| 05f72f9b6d | |||
| f9de1b9c00 | |||
| 9516e56242 | |||
| 5f315fc899 | |||
| e59606818d | |||
| e16104350e | |||
| df31191f4e | |||
| 5b310f6f93 | |||
| 0200f90e9a | |||
| 027e69e48f | |||
| 33e01e3805 | |||
| 13781dcd14 | |||
| 164bad437a | |||
| 7ddcc03ad9 | |||
| bbe4445257 | |||
| d90ddf889c | |||
| 5907164749 | |||
| eb6de433b7 | |||
| 32f72cdf55 | |||
| c160d97428 | |||
| 2a8de0565f | |||
| 5ab3c6e2d7 | |||
| 50ece576a7 | |||
| 9c202fa2d7 | |||
| e4e9cb09e4 | |||
| 6a7597c01a | |||
| dd05452edd | |||
| 99ea9f42e5 | |||
| 7d6a86adc7 | |||
| 7fbd2661c8 | |||
| 6ce678d1c2 | |||
| 5049b996db | |||
| d838456caa | |||
| e45c4ff4f1 | |||
| fa3959db17 | |||
| b97e206f7a | |||
| 7e9edecc7f | |||
| dc75b34deb | |||
| 8e22803797 | |||
| c290909eb3 | |||
| f2e56da2da | |||
| e6b258534a | |||
| acef002a2d | |||
| 11ebe014fb | |||
| 2d13c6e219 | |||
| fbcb492c79 | |||
| 4f67eff619 | |||
| 3c2c767e09 | |||
| ff527baa1d | |||
| 621718d4b1 | |||
| 59adc57344 | |||
| 82bd836ae9 | |||
| 1680613e12 | |||
| f6d3ce41bc | |||
| d16a48bf0f | |||
| 9459ce4030 | |||
| c773d1cd57 | |||
| 22a1aac84a | |||
| 3a20696da4 | |||
| 99bcf98617 | |||
| 09d9bae988 | |||
| 27de6106ab | |||
| aeded0a356 | |||
| afef1d56e8 | |||
| 4b55de27f1 | |||
| 3d7b6426a1 | |||
| 1d415eb7fb | |||
| 8a384a6d65 | |||
| 7c4dd2f2e7 | |||
| b6f0496c3c | |||
| 9b34650e72 | |||
| db9f1254b5 | |||
| b0e2ce6896 | |||
| 3afdedbd3f | |||
| 4bf69b97bd | |||
| ecb37fce91 | |||
| c1b27579ca | |||
| e64856c664 | |||
| de86598c0d | |||
| 553b45306f | |||
| d96a2485b6 | |||
| 9e9d1cc8cc | |||
| 579bb1b90c | |||
| 5116b4341e | |||
| 7ff64540a6 | |||
| 2e7192ab95 | |||
| 241d4cf94c | |||
| a5ce2dc1a1 | |||
| e21e3080e0 | |||
| 6a7ee90ff5 | |||
| 9d66893d5a | |||
| 64e780cf72 | |||
| 9bf141f698 | |||
| 9904094590 | |||
| 9e5b779abc | |||
| 729638a3bb | |||
| 82768382da | |||
| 3cb52447bb | |||
| 8d670b4ed7 | |||
| 624f6e0acc | |||
| c07cd8c252 | |||
| a10b29a8da | |||
| ea696f819e | |||
| e9b564a50c | |||
| fa16775ee2 | |||
| 55449c956a | |||
| e4b4e55dcd | |||
| 1c57de7e36 | |||
| dd2921fd26 | |||
| 977dbb5bcb | |||
| 49de462250 | |||
| 95e8add29b | |||
| b682fe631d | |||
| d32a648af5 | |||
| 50e9978dc3 | |||
| 17d407a26a | |||
| cdc3bd3f45 | |||
| 95b4192c0d | |||
| 4c911cd0eb | |||
| 7408999b0e | |||
| bb13bdaa80 | |||
| 10e7cbe006 | |||
| 0ad232e9de | |||
| 574cd11a40 | |||
| c04d6d946d | |||
| 36c2a985a6 | |||
| 91eff755fd | |||
| c05d287fcc | |||
| ea0be9e7a4 | |||
| 838ac273ab | |||
| 296e1c37e8 | |||
| 3117f11fae | |||
| 294c41f0dc | |||
| 60afbb2c20 | |||
| 19a45c856e | |||
| 3b784060b8 | |||
| dd5a11a61f | |||
| 9133b88d65 | |||
| 93b9afbd27 | |||
| 6c6bba2a04 | |||
| dd24b7e347 | |||
| ce896b9c83 | |||
| a3c1a61b59 | |||
| 98c62f4b1d | |||
| dd78cc9a50 | |||
| 9027411004 | |||
| 9394aa4649 | |||
| 88dc74bbe4 | |||
| 7023cec432 | |||
| 99d6fb9f5f | |||
| c9f723530d | |||
| 3205b571b0 | |||
| 2c4aef5272 | |||
| 4a45a7cc6b | |||
| ea8ab88056 | |||
| 564a21badd | |||
| 41ce87450f | |||
| fb0eef4200 | |||
| 819da37b89 | |||
| 8eb9c408a9 | |||
| 1f10b46402 | |||
| 7bb12b73e8 | |||
| b8103697c9 | |||
| ca3f8b5702 | |||
| daa01ea44b | |||
| 26d577f9c5 | |||
| 43fb711251 | |||
| 567af55a19 | |||
| c6a2e3b4d0 | |||
| a10428efe6 | |||
| 1440637e41 | |||
| 14cee66dfd | |||
| d048d60d04 | |||
| 1b9162151c | |||
| 885c9f1f06 | |||
| 90efcc8a8a | |||
| 7d35ee9998 | |||
| 584a7ac8a5 | |||
| f21e45ae64 | |||
| c7d26a27b6 | |||
| 54f9397f47 | |||
| ff44589572 | |||
| 53333b56ab | |||
| 6616b6299b | |||
| 0bab649156 | |||
| 2de7cb4726 | |||
| 99500d0cae | |||
| 7eb5478c42 | |||
| d2babb1331 | |||
| a6cced6b63 | |||
| 7917313422 | |||
| e02c22e136 | |||
| 2157644a39 | |||
| b6c2812a91 | |||
| 4b65d2c426 | |||
| 3589f3d5e7 | |||
| 1f39b8d239 | |||
| 7c3c390cbf | |||
| 19a1569fa8 | |||
| 9a0240bc7b | |||
| dd94a843b5 | |||
| 9426d08aa2 | |||
| fe2fac37f8 | |||
| cabd64df00 | |||
| bb57a54089 | |||
| 5e0bfba7bf | |||
| f6c19e469f | |||
| 13f09cc662 | |||
| a7e2856887 | |||
| 721e047485 | |||
| 301ff155a4 | |||
| 40f2065575 | |||
| be906eb823 | |||
| 2cee825f61 | |||
| ea6308bfdf | |||
| c6a6f7e2ab | |||
| 20077c285a | |||
| fa13016785 | |||
| b81bb9d621 | |||
| 2deb53c14f | |||
| 6b8feed08a | |||
| 2fe8e07cf3 | |||
| d2c907868e | |||
| a2900a5e09 | |||
| df934bacd9 | |||
| b0f03c278d | |||
| b59b321249 | |||
| 82d6991cf8 | |||
| d693d99379 | |||
| 50c1136be8 | |||
| dd491e516c | |||
| 078aab0d3d | |||
| 89c9f4b428 | |||
| 2148e896e2 | |||
| 7ba49b2e3e | |||
| 54a285f7e3 | |||
| 3ed20d059c | |||
| 92b6800f28 | |||
| 29cfd1a2dc | |||
| 45c09ea0ed | |||
| 424e24d530 | |||
| 26b2e3561a | |||
| 60aea6798d | |||
| fac3fe3f55 | |||
| 17fac0a488 | |||
| 6abc2207b2 | |||
| 885d224c5d | |||
| 12441deab8 | |||
| 8852fd14ce | |||
| e3814e6d9c | |||
| e19eba0942 | |||
| b6e2763731 | |||
| f75a3ee865 | |||
| d707c2f208 | |||
| a43b86d9eb | |||
| 5279d5e913 | |||
| df7fb92e03 | |||
| 95c3418313 | |||
| 67a8be9347 | |||
| 5d7f262f4a | |||
| 23eebc8127 | |||
| 3b435e460e | |||
| b52e9826e7 | |||
| ba49dd0499 | |||
| f9f6f28950 | |||
| f017015d1e | |||
| 29b4ee33bb | |||
| 03dc11c2f6 | |||
| b79ba5098b | |||
| 3c418d82e6 | |||
| 73ede2e344 | |||
| 266d891488 | |||
| c71490b472 | |||
| 07705079e2 | |||
| d88c268426 | |||
| a1b142e885 | |||
| a50d739e50 | |||
| f82bb56d89 | |||
| de593c3b93 | |||
| eedd5d80a0 | |||
| 5bbdd4f41e | |||
| 3fd24d96d3 | |||
| 7bea51fe09 | |||
| cdf0a38145 | |||
| bb2474ccbe | |||
| 01143a6f84 | |||
| b93485c2ed | |||
| 04681690b6 | |||
| 200877d691 | |||
| feb6417f45 | |||
| e7585a4ba2 | |||
| c33ab0f670 | |||
| 78181f66f6 | |||
| 925636be61 | |||
| 6be131d602 | |||
| f950f57eed | |||
| c6369feaea | |||
| 1bdd3f4908 | |||
| 924e095dfc | |||
| 72041a4a21 | |||
| d576446639 | |||
| 3fbec70ed3 | |||
| d177ab5ec2 | |||
| ba08f2c11e | |||
| 13a8aee049 | |||
| e4f5a284a6 | |||
| 6671b266da | |||
| 3728bd8e0f | |||
| c3616edbc8 | |||
| 21143b2693 | |||
| 006f9232cc | |||
| d1b61a34de | |||
| 896317ac97 | |||
| 4fd5c0edd4 | |||
| e2c5d6d857 | |||
| 8057fe4bc2 | |||
| ebc9690301 | |||
| 049ce1845e | |||
| faa6cd7deb | |||
| 1f9362c4e7 | |||
| 36fd18bcc8 | |||
| aa352317cb | |||
| edcd2cb50e | |||
| be8a64e4b8 | |||
| 2336bc0f65 | |||
| da82eecbae | |||
| 4855987ba8 | |||
| a493db1873 | |||
| 4f677a6cc2 | |||
| 228f2deb64 | |||
| 0fadf9a02c | |||
| 2b138ad76b | |||
| e3e4815b55 | |||
| f76f353c32 | |||
| 16b9f54dc3 | |||
| 281166475e | |||
| 23805cb5d6 | |||
| 76d0f9cfc1 | |||
| 5b083e4eb1 | |||
| f48694b18a | |||
| 8fa59ddbf0 | |||
| 2619db4026 | |||
| f3a71fb100 | |||
| 04fafe742c | |||
| cad8a41e6d | |||
| 1567923cda | |||
| c7116afcae | |||
| 4e184b2f90 | |||
| 9b5f664f00 | |||
| 0e89525ea8 | |||
| 0b6cdf8f02 | |||
| 5f63d959e1 | |||
| be7fe41179 | |||
| 204f343831 |
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -3,14 +3,12 @@
|
||||
/tests export-ignore
|
||||
/scripts export-ignore
|
||||
/.github export-ignore
|
||||
/.php_cs export-ignore
|
||||
/.php-cs-fixer.dist.php export-ignore
|
||||
.editorconfig export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.travis.yml export-ignore
|
||||
phpstan.neon export-ignore
|
||||
rector.yaml export-ignore
|
||||
phpunit.xml export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
CONTRIBUTING.md export-ignore
|
||||
README.md export-ignore
|
||||
|
||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,5 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: nunomaduro
|
||||
github: [nunomaduro,owenvoke,olivernybroe,octoper,lukeraymonddowning]
|
||||
patreon: nunomaduro
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L
|
||||
|
||||
21
.github/workflows/changelog.yml
vendored
21
.github/workflows/changelog.yml
vendored
@ -5,6 +5,7 @@ on:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- CHANGELOG.md
|
||||
- .github/workflows/changelog.yml
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
@ -13,19 +14,25 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: github.repository == 'pestphp/pest'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout website repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.CHANGELOG_KEY }}
|
||||
repository: pestphp/website
|
||||
path: pestphp-website
|
||||
repository: pestphp/docs
|
||||
path: pestphp-docs
|
||||
ref: master
|
||||
|
||||
- name: Read CHANGELOG.md
|
||||
id: package
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./CHANGELOG.md
|
||||
|
||||
- name: Add file headers
|
||||
uses: DamianReeves/write-file-action@v1.0
|
||||
with:
|
||||
@ -34,8 +41,6 @@ jobs:
|
||||
---
|
||||
title: Changelog
|
||||
description: Changelog
|
||||
extends: _layouts.documentation
|
||||
section: content
|
||||
---
|
||||
${{ steps.package.outputs.content }}
|
||||
|
||||
@ -43,14 +48,16 @@ jobs:
|
||||
|
||||
Next section: [Upgrade Guide →](/docs/upgrade-guide)
|
||||
write-mode: overwrite
|
||||
|
||||
- name: Copy CHANGELOG to website repository
|
||||
run: cp CHANGELOG.md pestphp-website/source/docs/changelog.md
|
||||
run: cp CHANGELOG.md pestphp-docs/changelog.md
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v2
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.CHANGELOG_KEY }}
|
||||
commit-message: Update changelog.md
|
||||
committer: GitHub Action <noreply@github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
|
||||
title: 'Update changelog.md'
|
||||
path: ./pestphp-website
|
||||
path: ./pestphp-docs
|
||||
|
||||
7
.github/workflows/static.yml
vendored
7
.github/workflows/static.yml
vendored
@ -15,16 +15,13 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 7.4
|
||||
php-version: 8.0
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer update --no-interaction --no-progress
|
||||
|
||||
- name: Run Rector
|
||||
run: vendor/bin/rector process src --dry-run
|
||||
|
||||
- name: Run PHP-CS-Fixer
|
||||
run: vendor/bin/php-cs-fixer fix -v --allow-risky=yes --dry-run
|
||||
|
||||
@ -43,7 +40,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 7.4
|
||||
php-version: 8.0
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
|
||||
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
@ -10,8 +10,9 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
dependency-version: [prefer-lowest, prefer-stable]
|
||||
parallel: ['', '--parallel']
|
||||
|
||||
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
|
||||
name: PHP ${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} - ${{ matrix.parallel }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -38,7 +39,7 @@ jobs:
|
||||
if: "matrix.php >= 8"
|
||||
|
||||
- name: Unit Tests
|
||||
run: php bin/pest --colors=always --exclude-group=integration
|
||||
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
|
||||
|
||||
- name: Integration Tests
|
||||
run: php bin/pest --colors=always --group=integration
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,7 +4,8 @@ composer.lock
|
||||
/vendor/
|
||||
coverage.xml
|
||||
.phpunit.result.cache
|
||||
.php_cs.cache
|
||||
/.php-cs-fixer.php
|
||||
.php-cs-fixer.cache
|
||||
.temp/coverage.php
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
@ -3,10 +3,9 @@
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'scripts')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
|
||||
->append(['.php_cs']);
|
||||
->append(['.php-cs-fixer.dist.php']);
|
||||
|
||||
$rules = [
|
||||
'@Symfony' => true,
|
||||
@ -25,7 +24,7 @@ $rules = [
|
||||
|
||||
$rules['increment_style'] = ['style' => 'post'];
|
||||
|
||||
return PhpCsFixer\Config::create()
|
||||
return (new PhpCsFixer\Config())
|
||||
->setUsingCache(true)
|
||||
->setRules($rules)
|
||||
->setFinder($finder);
|
||||
228
CHANGELOG.md
228
CHANGELOG.md
@ -4,7 +4,229 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
## [v1.16.0 (2021-08-19)](https://github.com/pestphp/pest/compare/v1.15.0...v1.16.0)
|
||||
### Added
|
||||
- Support for new parallel options ([#369](https://github.com/pestphp/pest/pull/369))
|
||||
|
||||
## [v1.15.0 (2021-08-04)](https://github.com/pestphp/pest/compare/v1.14.0...v1.15.0)
|
||||
### Added
|
||||
- `toBeTruthy` and `toBeFalsy` ([#367](https://github.com/pestphp/pest/pull/367))
|
||||
|
||||
## [v1.14.0 (2021-08-03)](https://github.com/pestphp/pest/compare/v1.13.0...v1.14.0)
|
||||
### Added
|
||||
- A new bound closure that allows you to access the test case in Datasets ([#364](https://github.com/pestphp/pest/pull/364))
|
||||
|
||||
## [v1.13.0 (2021-08-02)](https://github.com/pestphp/pest/compare/v1.12.0...v1.13.0)
|
||||
### Added
|
||||
- A cleaner output when running the Pest runner in PhpStorm ([#350](https://github.com/pestphp/pest/pull/350))
|
||||
- `toBeIn` expectation ([#363](https://github.com/pestphp/pest/pull/363))
|
||||
|
||||
### Fixed
|
||||
- `skip` with false condition marking test as skipped ([22b822c](https://github.com/pestphp/pest/commit/22b822ce87a3d19d84960fa5c93eb286820b525d))
|
||||
|
||||
## [v1.12.0 (2021-07-26)](https://github.com/pestphp/pest/compare/v1.11.0...v1.12.0)
|
||||
### Added
|
||||
- `--force` option to override tests in `pest:test` artisan command ([#353](https://github.com/pestphp/pest/pull/353))
|
||||
- Support for PHPUnit `^9.3.7` ([ca9d783](https://github.com/pestphp/pest/commit/ca9d783cf942a2caabc85ff7a728c7f28350c67a))
|
||||
|
||||
### Fixed
|
||||
- `beforeAll` and `afterAll` behind called multiple times per test ([#357](https://github.com/pestphp/pest/pull/357))
|
||||
|
||||
## [v1.11.0 (2021-07-21)](https://github.com/pestphp/pest/compare/v1.10.0...v1.11.0)
|
||||
### Added
|
||||
- Support for interacting with datasets in higher order tests ([#352](https://github.com/pestphp/pest/pull/352))
|
||||
|
||||
### Changed
|
||||
- The unit test stub now uses the expectation API ([#348](https://github.com/pestphp/pest/pull/348))
|
||||
|
||||
### Fixed
|
||||
- PhpStorm will no longer show 0 assertions in the output ([#349](https://github.com/pestphp/pest/pull/349))
|
||||
|
||||
## [v1.10.0 (2021-07-12)](https://github.com/pestphp/pest/compare/v1.9.1...v1.10.0)
|
||||
### Added
|
||||
- The ability to use higher order expectations inside higher order tests ([#341](https://github.com/pestphp/pest/pull/341))
|
||||
|
||||
## [v1.9.1 (2021-07-11)](https://github.com/pestphp/pest/compare/v1.9.0...v1.9.1)
|
||||
### Fixed
|
||||
- Callable `expect` values in higher order tests failing if the value was an existing method name ([#334](https://github.com/pestphp/pest/pull/344))
|
||||
|
||||
## [v1.9.0 (2021-07-09)](https://github.com/pestphp/pest/compare/v1.8.0...v1.9.0)
|
||||
### Changed
|
||||
- You may now pass just an exception message when using the `throws` method ([#339](https://github.com/pestphp/pest/pull/339))
|
||||
|
||||
## [v1.8.0 (2021-07-08)](https://github.com/pestphp/pest/compare/v1.7.1...v1.8.0)
|
||||
### Added
|
||||
- A new `tap` and test case aware `expect` methods for higher order tests ([#331](https://github.com/pestphp/pest/pull/331))
|
||||
- Access to test case methods and properties when using `skip` ([#338](https://github.com/pestphp/pest/pull/338))
|
||||
|
||||
## [v1.7.1 (2021-06-24)](https://github.com/pestphp/pest/compare/v1.7.0...v1.7.1)
|
||||
### Fixed
|
||||
- The `and` method not being usable in Higher Order expectations ([#330](https://github.com/pestphp/pest/pull/330))
|
||||
|
||||
## [v1.7.0 (2021-06-19)](https://github.com/pestphp/pest/compare/v1.6.0...v1.7.0)
|
||||
### Added
|
||||
- Support for non-callable values in the sequence method, which will be passed as `toEqual` ([#323](https://github.com/pestphp/pest/pull/323))
|
||||
- Support for nested Higher Order Expectations ([#324](https://github.com/pestphp/pest/pull/324))
|
||||
|
||||
## [v1.6.0 (2021-06-18)](https://github.com/pestphp/pest/compare/v1.5.0...v1.6.0)
|
||||
### Added
|
||||
- Adds a new `json` expectation method to improve testing with JSON strings ([#325](https://github.com/pestphp/pest/pull/325))
|
||||
- Adds dot notation support to the `toHaveKey` and `toHaveKeys` expectations ([#322](https://github.com/pestphp/pest/pull/322))
|
||||
|
||||
## [v1.5.0 (2021-06-15)](https://github.com/pestphp/pest/compare/v1.4.0...v1.5.0)
|
||||
### Changed
|
||||
- Moves plugins from the `require` section to the core itself ([#317](https://github.com/pestphp/pest/pull/317)), ([#318](https://github.com/pestphp/pest/pull/318)), ([#320](https://github.com/pestphp/pest/pull/320))
|
||||
|
||||
## [v1.4.0 (2021-06-10)](https://github.com/pestphp/pest/compare/v1.3.2...v1.4.0)
|
||||
### Added
|
||||
- Support for multiple datasets (Matrix) on the `with` method ([#303](https://github.com/pestphp/pest/pull/303))
|
||||
- Support for incompleted tests ([49de462](https://github.com/pestphp/pest/commit/49de462250cf9f65f09e13eaf6dcc0e06865b930))
|
||||
|
||||
## [v1.3.2 (2021-06-07)](https://github.com/pestphp/pest/compare/v1.3.1...v1.3.2)
|
||||
### Fixed
|
||||
- Test cases with the @ symbol in the directory fail ([#308](https://github.com/pestphp/pest/pull/308))
|
||||
|
||||
## [v1.3.1 (2021-06-06)](https://github.com/pestphp/pest/compare/v1.3.0...v1.3.1)
|
||||
### Added
|
||||
- Added for PHPUnit 9.5.5 ([#310](https://github.com/pestphp/pest/pull/310))
|
||||
|
||||
### Changed
|
||||
- Lock minimum Pest plugin versions ([#306](https://github.com/pestphp/pest/pull/306))
|
||||
|
||||
## [v1.3.0 (2021-05-23)](https://github.com/pestphp/pest/compare/v1.2.1...v1.3.0)
|
||||
### Added
|
||||
- Named datasets no longer show the arguments ([#302](https://github.com/pestphp/pest/pull/302))
|
||||
|
||||
### Fixed
|
||||
- Wraps global functions within `function_exists` ([#300](https://github.com/pestphp/pest/pull/300))
|
||||
|
||||
## [v1.2.1 (2021-05-14)](https://github.com/pestphp/pest/compare/v1.2.0...v1.2.1)
|
||||
### Fixed
|
||||
- Laravel commands failing with new `--test-directory` option ([#297](https://github.com/pestphp/pest/pull/297))
|
||||
|
||||
## [v1.2.0 (2021-05-13)](https://github.com/pestphp/pest/compare/v1.1.0...v1.2.0)
|
||||
### Added
|
||||
- Adds JUnit / Infection support ([#291](https://github.com/pestphp/pest/pull/291))
|
||||
- `--test-directory` command line option ([#283](https://github.com/pestphp/pest/pull/283))
|
||||
|
||||
## [v1.1.0 (2021-05-02)](https://github.com/pestphp/pest/compare/v1.0.5...v1.1.0)
|
||||
### Added
|
||||
- Possibility of "hooks" being added using the "uses" function ([#282](https://github.com/pestphp/pest/pull/282))
|
||||
|
||||
## [v1.0.5 (2021-03-31)](https://github.com/pestphp/pest/compare/v1.0.4...v1.0.5)
|
||||
### Added
|
||||
- Add `--browse` option to `pest:dusk` command ([#280](https://github.com/pestphp/pest/pull/280))
|
||||
- Support for PHPUnit 9.5.4 ([#284](https://github.com/pestphp/pest/pull/284))
|
||||
|
||||
## [v1.0.4 (2021-03-17)](https://github.com/pestphp/pest/compare/v1.0.3...v1.0.4)
|
||||
### Added
|
||||
- Support for PHPUnit 9.5.3 ([#278](https://github.com/pestphp/pest/pull/278))
|
||||
|
||||
## [v1.0.3 (2021-03-13)](https://github.com/pestphp/pest/compare/v1.0.2...v1.0.3)
|
||||
### Added
|
||||
- Support for test extensions ([#269](https://github.com/pestphp/pest/pull/269))
|
||||
|
||||
## [v1.0.2 (2021-02-04)](https://github.com/pestphp/pest/compare/v1.0.1...v1.0.2)
|
||||
### Added
|
||||
- Support for PHPUnit 9.5.2 ([#267](https://github.com/pestphp/pest/pull/267))
|
||||
|
||||
## [v1.0.1 (2021-01-18)](https://github.com/pestphp/pest/compare/v1.0.0...v1.0.1)
|
||||
### Added
|
||||
- Support for PHPUnit 9.5.1 ([#261](https://github.com/pestphp/pest/pull/261))
|
||||
|
||||
### Fixed
|
||||
- Fix `TestCase@expect` PHPDoc tag ([#251](https://github.com/pestphp/pest/pull/251))
|
||||
|
||||
## [v1.0.0 (2021-01-03)](https://github.com/pestphp/pest/compare/v0.3.19...v1.0.0)
|
||||
### Added
|
||||
- `pest:test --dusk` option ([#245](https://github.com/pestphp/pest/pull/245))
|
||||
|
||||
### Changed
|
||||
- Stable version
|
||||
- Updates init structure ([#240](https://github.com/pestphp/pest/pull/240))
|
||||
|
||||
## [v0.3.19 (2020-12-27)](https://github.com/pestphp/pest/compare/v0.3.18...v0.3.19)
|
||||
### Fixed
|
||||
- Fix binary path in `pest:dusk` command ([#239](https://github.com/pestphp/pest/pull/239))
|
||||
|
||||
## [v0.3.18 (2020-12-26)](https://github.com/pestphp/pest/compare/v0.3.17...v0.3.18)
|
||||
### Added
|
||||
- `toBeJson()` expectation ([plugin-expectations#2](https://github.com/pestphp/pest-plugin-expectations/pull/2))
|
||||
|
||||
## [v0.3.17 (2020-12-20)](https://github.com/pestphp/pest/compare/v0.3.16...v0.3.17)
|
||||
### Fixed
|
||||
- Class inheritance with `depends()` ([#236](https://github.com/pestphp/pest/pull/236))
|
||||
|
||||
## [v0.3.16 (2020-12-13)](https://github.com/pestphp/pest/compare/v0.3.15...v0.3.16)
|
||||
### Changed
|
||||
- Moves expectation API for external plugin ([5d7f262](https://github.com/pestphp/pest/commit/5d7f262f4ab280a660a85900f402eebb23abfda8))
|
||||
|
||||
## [v0.3.15 (2020-12-04)](https://github.com/pestphp/pest/compare/v0.3.14...v0.3.15)
|
||||
### Added
|
||||
- Support for PHPUnit 9.5.0 ([#234](https://github.com/pestphp/pest/pull/234))
|
||||
- Support for extending expectation API ([#232](https://github.com/pestphp/pest/pull/232))
|
||||
|
||||
### Fixed
|
||||
- Static analysis while using string as key for datasets ([#233](https://github.com/pestphp/pest/pull/233))
|
||||
|
||||
## [v0.3.14 (2020-11-28)](https://github.com/pestphp/pest/compare/v0.3.13...v0.3.14)
|
||||
### Added
|
||||
- `pest:dusk` command ([#223](https://github.com/pestphp/pest/pull/223))
|
||||
- Better feedback on errors in `toMatchArray` and `toMatchObject` ([#231](https://github.com/pestphp/pest/pull/231))
|
||||
|
||||
## [v0.3.13 (2020-11-23)](https://github.com/pestphp/pest/compare/v0.3.12...v0.3.13)
|
||||
### Added
|
||||
- `toMatchArray` expectation ([7bea51f](https://github.com/pestphp/pest/commit/7bea51fe09dd2eca7093e4c34cf2dab2e8d39fa5), [3fd24d9](https://github.com/pestphp/pest/commit/3fd24d96d3145dcebdb0aab40aa8b76faa8b6979))
|
||||
- Add Pest options to `--help` output ([#217](https://github.com/pestphp/pest/pull/217))
|
||||
|
||||
### Fixed
|
||||
- Resolve issue with name resolution in `depends()` ([#216](https://github.com/pestphp/pest/pull/216))
|
||||
|
||||
## [v0.3.12 (2020-11-11)](https://github.com/pestphp/pest/compare/v0.3.11...v0.3.12)
|
||||
### Added
|
||||
- Add support for PHPUnit 9.4.3 ([#219](https://github.com/pestphp/pest/pull/219))
|
||||
|
||||
## [v0.3.11 (2020-11-09)](https://github.com/pestphp/pest/compare/v0.3.10...v0.3.11)
|
||||
### Changed
|
||||
- Improved the exception output for the TeamCity printer (usage with phpstorm plugin) ([#215](https://github.com/pestphp/pest/pull/215))
|
||||
|
||||
## [v0.3.10 (2020-11-01)](https://github.com/pestphp/pest/compare/v0.3.9...v0.3.10)
|
||||
### Added
|
||||
- Add support for PHPUnit 9.4.2 ([d177ab5](https://github.com/pestphp/pest/commit/d177ab5ec2030c5bb8e418d10834c370c94c433d))
|
||||
|
||||
## [v0.3.9 (2020-10-13)](https://github.com/pestphp/pest/compare/v0.3.8...v0.3.9)
|
||||
### Added
|
||||
- Add support for named datasets in description output ([#134](https://github.com/pestphp/pest/pull/134))
|
||||
- Add Pest version to `--help` output ([#203](https://github.com/pestphp/pest/pull/203))
|
||||
- Add support for PHPUnit 9.4.1 ([#207](https://github.com/pestphp/pest/pull/207))
|
||||
|
||||
## [v0.3.8 (2020-10-03)](https://github.com/pestphp/pest/compare/v0.3.7...v0.3.8)
|
||||
### Added
|
||||
- Add support for PHPUnit 9.4.0 ([#199](https://github.com/pestphp/pest/pull/199))
|
||||
|
||||
### Fixed
|
||||
- Fix chained higher order assertions returning void ([#196](https://github.com/pestphp/pest/pull/196))
|
||||
|
||||
## [v0.3.7 (2020-09-30)](https://github.com/pestphp/pest/compare/v0.3.6...v0.3.7)
|
||||
### Added
|
||||
- Add support for PHPUnit 9.3.11 ([#193](https://github.com/pestphp/pest/pull/193))
|
||||
|
||||
## [v0.3.6 (2020-09-21)](https://github.com/pestphp/pest/compare/v0.3.5...v0.3.6)
|
||||
### Added
|
||||
- `toMatch` expectation ([#191](https://github.com/pestphp/pest/pull/191))
|
||||
- `toMatchConstraint` expectation ([#190](https://github.com/pestphp/pest/pull/190))
|
||||
|
||||
## [v0.3.5 (2020-09-16)](https://github.com/pestphp/pest/compare/v0.3.4...v0.3.5)
|
||||
### Added
|
||||
- `toStartWith` and `toEndWith` expectations ([#187](https://github.com/pestphp/pest/pull/187))
|
||||
|
||||
## [v0.3.4 (2020-09-15)](https://github.com/pestphp/pest/compare/v0.3.3...v0.3.4)
|
||||
### Added
|
||||
- `toMatchObject` expectation ([4e184b2](https://github.com/pestphp/pest/commit/4e184b2f906c318a5e9cd38fe693cdab5c48d8a2))
|
||||
|
||||
## [v0.3.3 (2020-09-13)](https://github.com/pestphp/pest/compare/v0.3.2...v0.3.3)
|
||||
### Added
|
||||
- `toHaveKeys` expectation ([204f343](https://github.com/pestphp/pest/commit/204f343831adc17bb3734553c24fac92d02f27c7))
|
||||
|
||||
## [v0.3.2 (2020-09-12)](https://github.com/pestphp/pest/compare/v0.3.1...v0.3.2)
|
||||
### Added
|
||||
@ -18,7 +240,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
### Added
|
||||
- Expectation API (TODO)
|
||||
- PHPUnit 9.3 and PHP 8 support ([#128](https://github.com/pestphp/pest/pull/128))
|
||||
- Fowards `$this` calls to globals ([#169](https://github.com/pestphp/pest/pull/169))
|
||||
- Forwards `$this` calls to globals ([#169](https://github.com/pestphp/pest/pull/169))
|
||||
|
||||
### Fixed
|
||||
- don't decorate output if --colors=never is set ([36b879f](https://github.com/pestphp/pest/commit/36b879f97d7b187c87a94eb60af5b7d3b7253d56))
|
||||
@ -33,7 +255,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- `depends` phpunit feature ([#103](https://github.com/pestphp/pest/pull/103))
|
||||
|
||||
### Fixes
|
||||
- datasets name conflit ([#101](https://github.com/pestphp/pest/pull/101))
|
||||
- datasets name conflict ([#101](https://github.com/pestphp/pest/pull/101))
|
||||
|
||||
## [v0.2.1 (2020-06-17)](https://github.com/pestphp/pest/compare/v0.2.0...v0.2.1)
|
||||
### Fixes
|
||||
|
||||
@ -21,6 +21,10 @@ We would like to extend our thanks to the following sponsors for funding Pest de
|
||||
|
||||
### Premium Sponsors
|
||||
|
||||
- **[Scout APM](https://github.com/scoutapp)**
|
||||
- **[Akaunting](https://akaunting.com)**
|
||||
- **[Codecourse](https://codecourse.com/)**
|
||||
- **[Meema](https://meema.io/)**
|
||||
- **[Scout APM](https://scoutapm.com)**
|
||||
- **[Spatie](https://spatie.be/)**
|
||||
|
||||
Pest was created by **[Nuno Maduro](https://twitter.com/enunomaduro)** under the **[Sponsorware license](https://github.com/sponsorware/docs)**. It got open-sourced and is now 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)**.
|
||||
|
||||
16
RELEASE.md
Normal file
16
RELEASE.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Release process
|
||||
|
||||
When releasing a new version of Pest there are some checks and updates that need to be done:
|
||||
|
||||
- Clear your local repository with: `git add . && git reset --hard && git checkout master`
|
||||
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...master](https://github.com/pestphp/pest/compare/{latest_version}...master) and update the [changelog](CHANGELOG.md) file with the main changes for this release
|
||||
- Update the version number in [src/Pest.php](src/Pest.php)
|
||||
- Run the tests locally using: `composer test`
|
||||
- Commit the CHANGELOG and Pest file with the message: `git commit -m "release: vX.X.X"`
|
||||
- Push the changes to GitHub
|
||||
- Check that the CI is passing as expected: [github.com/pestphp/pest/actions](https://github.com/pestphp/pest/actions)
|
||||
- Tag and push the tag with `git tag vX.X.X && git push --tags`
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins should be versioned using the same major (or minor for `0.x` releases) version as Pest core.
|
||||
31
bin/pest
31
bin/pest
@ -3,9 +3,9 @@
|
||||
|
||||
use NunoMaduro\Collision\Provider;
|
||||
use Pest\Actions\ValidatesEnvironment;
|
||||
use Pest\Console\Command;
|
||||
use Pest\Support\Container;
|
||||
use Pest\TestSuite;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@ -19,17 +19,21 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
if (file_exists($vendorPath)) {
|
||||
include_once $vendorPath;
|
||||
$autoloadPath = $vendorPath;
|
||||
} else {
|
||||
include_once $localPath;
|
||||
$autoloadPath = $localPath;
|
||||
}
|
||||
|
||||
(new Provider())->register();
|
||||
|
||||
$rootPath = getcwd();
|
||||
// Get $rootPath based on $autoloadPath
|
||||
$rootPath = dirname($autoloadPath, 2);
|
||||
$argv = new ArgvInput();
|
||||
|
||||
$testSuite = TestSuite::getInstance($rootPath);
|
||||
$testSuite = TestSuite::getInstance($rootPath, $argv->getParameterOption('--test-directory', 'tests'));
|
||||
|
||||
$isDecorated = (new ArgvInput())->getParameterOption('--colors', 'always') !== 'never';
|
||||
$isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never';
|
||||
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
||||
|
||||
$container = Container::getInstance();
|
||||
@ -38,5 +42,22 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
ValidatesEnvironment::in($testSuite);
|
||||
|
||||
exit($container->get(Command::class)->run($_SERVER['argv']));
|
||||
$args = $_SERVER['argv'];
|
||||
|
||||
// Let's remove any arguments that PHPUnit does not understand
|
||||
if ($argv->hasParameterOption('--test-directory')) {
|
||||
foreach ($args as $key => $value) {
|
||||
if (strpos($value, '--test-directory') !== false) {
|
||||
unset($args[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (($runInParallel = $argv->hasParameterOption(['--parallel', '-p'])) && !class_exists(\Pest\Parallel\Command::class)) {
|
||||
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
|
||||
exit(Command::FAILURE);
|
||||
}
|
||||
|
||||
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class;
|
||||
exit($container->get($command)->run($args));
|
||||
})();
|
||||
|
||||
@ -18,18 +18,16 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.3 || ^8.0",
|
||||
"nunomaduro/collision": "^5.0",
|
||||
"pestphp/pest-plugin": "^0.3",
|
||||
"pestphp/pest-plugin-coverage": "^0.3",
|
||||
"pestphp/pest-plugin-init": "^0.3",
|
||||
"phpunit/phpunit": "9.3.7 || 9.3.8 || 9.3.9 || 9.3.10"
|
||||
"nunomaduro/collision": "^5.4.0",
|
||||
"pestphp/pest-plugin": "^1.0.0",
|
||||
"phpunit/phpunit": "^9.5.5"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Pest\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/globals.php",
|
||||
"src/Functions.php",
|
||||
"src/Pest.php"
|
||||
]
|
||||
},
|
||||
@ -42,10 +40,11 @@
|
||||
]
|
||||
},
|
||||
"require-dev": {
|
||||
"illuminate/console": "^7.16.1",
|
||||
"illuminate/support": "^7.16.1",
|
||||
"mockery/mockery": "^1.4.1",
|
||||
"pestphp/pest-dev-tools": "dev-master"
|
||||
"illuminate/console": "^8.47.0",
|
||||
"illuminate/support": "^8.47.0",
|
||||
"laravel/dusk": "^6.15.0",
|
||||
"pestphp/pest-dev-tools": "dev-master",
|
||||
"pestphp/pest-plugin-parallel": "^1.0"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
@ -57,25 +56,29 @@
|
||||
"bin/pest"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "rector process src && php-cs-fixer fix -v",
|
||||
"test:lint": "php-cs-fixer fix -v --dry-run && rector process src --dry-run",
|
||||
"test:types": "phpstan analyse --ansi --memory-limit=0",
|
||||
"lint": "php-cs-fixer fix -v",
|
||||
"test:lint": "php-cs-fixer fix -v --dry-run",
|
||||
"test:types": "phpstan analyse --ansi --memory-limit=-1",
|
||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
|
||||
"test:parallel": "php bin/pest -p --colors=always --exclude-group=integration",
|
||||
"test:integration": "php bin/pest --colors=always --group=integration",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
|
||||
"test": [
|
||||
"@test:lint",
|
||||
"@test:types",
|
||||
"@test:unit",
|
||||
"@test:parallel",
|
||||
"@test:integration"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "0.4.x-dev"
|
||||
"dev-master": "1.x-dev"
|
||||
},
|
||||
"pest": {
|
||||
"plugins": [
|
||||
"Pest\\Plugins\\Coverage",
|
||||
"Pest\\Plugins\\Init",
|
||||
"Pest\\Plugins\\Version"
|
||||
]
|
||||
},
|
||||
|
||||
15
phpstan.neon
15
phpstan.neon
@ -7,15 +7,13 @@ parameters:
|
||||
level: max
|
||||
paths:
|
||||
- src
|
||||
excludes_analyse:
|
||||
- src/globals.php
|
||||
|
||||
checkMissingIterableValueType: true
|
||||
checkGenericClassInNonGenericObjectType: false
|
||||
reportUnmatchedIgnoredErrors: true
|
||||
|
||||
ignoreErrors:
|
||||
- "#Undefined variable: \\$this#"
|
||||
- "#type mixed is not subtype of native#"
|
||||
- "#is not allowed to extend#"
|
||||
- "#Language construct eval#"
|
||||
- "# with null as default value#"
|
||||
@ -24,10 +22,13 @@ parameters:
|
||||
- "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#"
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#'
|
||||
path: src/TeamCity.php
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#invalid typehint type Pest\\Concerns\\TestCase#'
|
||||
path: src/TeamCity.php
|
||||
message: '#invalid typehint type Pest\\Concerns\\Testable#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#is not subtype of native type PHPUnit\\Framework\\Test#'
|
||||
path: src/TeamCity.php
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getPrintableTestCaseName\(\)#'
|
||||
path: src/Logging
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
<directory suffix=".php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage>
|
||||
<include processUncoveredFiles="true">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
|
||||
35
rector.php
35
rector.php
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Core\Configuration\Option;
|
||||
use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector;
|
||||
use Rector\Set\ValueObject\SetList;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return static function (ContainerConfigurator $containerConfigurator): void {
|
||||
$parameters = $containerConfigurator->parameters();
|
||||
|
||||
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
|
||||
|
||||
$parameters->set(Option::SETS, [
|
||||
SetList::ACTION_INJECTION_TO_CONSTRUCTOR_INJECTION,
|
||||
SetList::ARRAY_STR_FUNCTIONS_TO_STATIC_CALL,
|
||||
SetList::CODE_QUALITY,
|
||||
SetList::PHP_53,
|
||||
SetList::PHP_54,
|
||||
SetList::PHP_56,
|
||||
SetList::PHP_70,
|
||||
SetList::PHP_71,
|
||||
SetList::PHP_72,
|
||||
SetList::PHP_73,
|
||||
SetList::PHPSTAN,
|
||||
SetList::PHPUNIT_CODE_QUALITY,
|
||||
SetList::SOLID,
|
||||
]);
|
||||
|
||||
$parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']);
|
||||
$parameters->set(Option::EXCLUDE_RECTORS, [
|
||||
StaticCallOnNonStaticToInstanceCallRector::class,
|
||||
]);
|
||||
};
|
||||
@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$globalsFilePath = implode(DIRECTORY_SEPARATOR, [
|
||||
dirname(__DIR__),
|
||||
'vendor',
|
||||
'phpunit',
|
||||
'phpunit',
|
||||
'src',
|
||||
'Framework',
|
||||
'Assert',
|
||||
'Functions.php',
|
||||
]);
|
||||
|
||||
$compiledFilePath = implode(DIRECTORY_SEPARATOR, [dirname(__DIR__), 'compiled', 'globals.php']);
|
||||
|
||||
@unlink($compiledFilePath);
|
||||
|
||||
$replace = function ($contents, $string, $by) {
|
||||
return str_replace($string, $by, $contents);
|
||||
};
|
||||
|
||||
$remove = function ($contents, $string) {
|
||||
return str_replace($string, '', $contents);
|
||||
};
|
||||
|
||||
$contents = file_get_contents($globalsFilePath);
|
||||
$contents = $replace($contents, 'namespace PHPUnit\Framework;', 'use PHPUnit\Framework\Assert;');
|
||||
$contents = $remove($contents, 'use ArrayAccess;');
|
||||
$contents = $remove($contents, 'use Countable;');
|
||||
$contents = $remove($contents, 'use DOMDocument;');
|
||||
$contents = $remove($contents, 'use DOMElement;');
|
||||
$contents = $remove($contents, 'use Throwable;');
|
||||
|
||||
file_put_contents(implode(DIRECTORY_SEPARATOR, [dirname(__DIR__), 'compiled', 'globals.php']), $contents);
|
||||
@ -5,7 +5,8 @@ declare(strict_types=1);
|
||||
namespace Pest\Actions;
|
||||
|
||||
use NunoMaduro\Collision\Adapters\Phpunit\Printer;
|
||||
use Pest\TeamCity;
|
||||
use Pest\Logging\JUnit;
|
||||
use Pest\Logging\TeamCity;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
|
||||
/**
|
||||
@ -29,7 +30,15 @@ final class AddsDefaults
|
||||
}
|
||||
|
||||
if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) {
|
||||
$arguments[self::PRINTER] = new TeamCity($arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
|
||||
$arguments[self::PRINTER] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
|
||||
}
|
||||
|
||||
// Load our junit logger instead.
|
||||
if (array_key_exists('junitLogfile', $arguments)) {
|
||||
$arguments['listeners'][] = new JUnit(
|
||||
$arguments['junitLogfile']
|
||||
);
|
||||
unset($arguments['junitLogfile']);
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
|
||||
@ -22,8 +22,6 @@ final class AddsTests
|
||||
{
|
||||
self::removeTestClosureWarnings($testSuite);
|
||||
|
||||
// @todo refactor this...
|
||||
|
||||
$testSuites = [];
|
||||
$pestTestSuite->tests->build($pestTestSuite, function (TestCase $testCase) use (&$testSuites): void {
|
||||
$testCaseClass = get_class($testCase);
|
||||
|
||||
50
src/Actions/InteractsWithPlugins.php
Normal file
50
src/Actions/InteractsWithPlugins.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
|
||||
use Pest\Contracts\Plugins\AddsOutput;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Plugin\Loader;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InteractsWithPlugins
|
||||
{
|
||||
/**
|
||||
* Transform the input arguments by passing it to the relevant plugins.
|
||||
*
|
||||
* @param array<int, string> $argv
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function handleArguments(array $argv): array
|
||||
{
|
||||
$plugins = Loader::getPlugins(HandlesArguments::class);
|
||||
|
||||
/** @var HandlesArguments $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$argv = $plugin->handleArguments($argv);
|
||||
}
|
||||
|
||||
return $argv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an opportunity for any plugins that want
|
||||
* to provide additional output after test execution.
|
||||
*/
|
||||
public static function addOutput(int $result): int
|
||||
{
|
||||
$plugins = Loader::getPlugins(AddsOutput::class);
|
||||
|
||||
/** @var AddsOutput $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$result = $plugin->addOutput($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Actions;
|
||||
|
||||
use Pest\Support\Str;
|
||||
use PHPUnit\TextUI\Configuration\Configuration;
|
||||
use function Pest\testDirectory;
|
||||
use PHPUnit\Util\FileLoader;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
@ -21,6 +21,7 @@ final class LoadStructure
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const STRUCTURE = [
|
||||
'Expectations.php',
|
||||
'Datasets.php',
|
||||
'Helpers.php',
|
||||
'Pest.php',
|
||||
@ -32,7 +33,7 @@ final class LoadStructure
|
||||
*/
|
||||
public static function in(string $rootPath): void
|
||||
{
|
||||
$testsPath = $rootPath . DIRECTORY_SEPARATOR . 'tests';
|
||||
$testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
|
||||
|
||||
$load = function ($filename): bool {
|
||||
return file_exists($filename) && (bool) FileLoader::checkAndLoad($filename);
|
||||
|
||||
27
src/Concerns/Expectable.php
Normal file
27
src/Concerns/Expectable.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Concerns;
|
||||
|
||||
use Pest\Expectation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait Expectable
|
||||
{
|
||||
/**
|
||||
* @template TValue
|
||||
*
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function expect($value): Expectation
|
||||
{
|
||||
return new Expectation($value);
|
||||
}
|
||||
}
|
||||
54
src/Concerns/Extendable.php
Normal file
54
src/Concerns/Extendable.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Concerns;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait Extendable
|
||||
{
|
||||
/**
|
||||
* @var array<string, Closure>
|
||||
*/
|
||||
private static $extends = [];
|
||||
|
||||
/**
|
||||
* Register a custom extend.
|
||||
*/
|
||||
public static function extend(string $name, Closure $extend): void
|
||||
{
|
||||
static::$extends[$name] = $extend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if extend is registered.
|
||||
*/
|
||||
public static function hasExtend(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, static::$extends);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically handle calls to the class.
|
||||
*
|
||||
* @param array<int, mixed> $parameters
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $parameters)
|
||||
{
|
||||
if (!static::hasExtend($method)) {
|
||||
throw new BadMethodCallException("$method is not a callable method name.");
|
||||
}
|
||||
|
||||
/** @var Closure $extend */
|
||||
$extend = static::$extends[$method]->bindTo($this, static::class);
|
||||
|
||||
return $extend(...$parameters);
|
||||
}
|
||||
}
|
||||
33
src/Concerns/Logging/WritesToConsole.php
Normal file
33
src/Concerns/Logging/WritesToConsole.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Concerns\Logging;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait WritesToConsole
|
||||
{
|
||||
private function writeSuccess(string $message): void
|
||||
{
|
||||
$this->writePestTestOutput($message, 'fg-green, bold', '✓');
|
||||
}
|
||||
|
||||
private function writeError(string $message): void
|
||||
{
|
||||
$this->writePestTestOutput($message, 'fg-red, bold', '⨯');
|
||||
}
|
||||
|
||||
private function writeWarning(string $message): void
|
||||
{
|
||||
$this->writePestTestOutput($message, 'fg-yellow, bold', '-');
|
||||
}
|
||||
|
||||
private function writePestTestOutput(string $message, string $color, string $symbol): void
|
||||
{
|
||||
$this->writeWithColor($color, "$symbol ", false);
|
||||
$this->write($message);
|
||||
$this->writeNewLine();
|
||||
}
|
||||
}
|
||||
31
src/Concerns/RetrievesValues.php
Normal file
31
src/Concerns/RetrievesValues.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Concerns;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait RetrievesValues
|
||||
{
|
||||
/**
|
||||
* @template TRetrievableValue
|
||||
*
|
||||
* Safely retrieve the value at the given key from an object or array.
|
||||
*
|
||||
* @param array<string, TRetrievableValue>|object $value
|
||||
* @param TRetrievableValue|null $default
|
||||
*
|
||||
* @return TRetrievableValue|null
|
||||
*/
|
||||
private function retrieve(string $key, $value, $default = null)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value[$key] ?? $default;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return $value->$key ?? $default;
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Pest\Expectation;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Util\Test;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* To avoid inheritance conflicts, all the fields related
|
||||
* to Pest only will be prefixed by double underscore.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait TestCase
|
||||
{
|
||||
/**
|
||||
* The test case description. Contains the first
|
||||
* argument of global functions like `it` and `test`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $__description;
|
||||
|
||||
/**
|
||||
* Holds the test closure function.
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
private $__test;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the test case.
|
||||
*/
|
||||
public function __construct(Closure $test, string $description, array $data)
|
||||
{
|
||||
$this->__test = $test;
|
||||
$this->__description = $description;
|
||||
|
||||
parent::__construct('__test', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the groups to the current test case.
|
||||
*/
|
||||
public function addGroups(array $groups): void
|
||||
{
|
||||
$groups = array_unique(array_merge($this->getGroups(), $groups));
|
||||
|
||||
$this->setGroups($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test case name. Note that, in Pest
|
||||
* we ignore withDataset argument as the description
|
||||
* already contains the dataset description.
|
||||
*/
|
||||
public function getName(bool $withDataSet = true): string
|
||||
{
|
||||
return $this->__description;
|
||||
}
|
||||
|
||||
public static function __getFileName(): string
|
||||
{
|
||||
return self::$__filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before the first test of this test class is run.
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
|
||||
|
||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after the last test of this test class is run.
|
||||
*/
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
|
||||
|
||||
call_user_func(Closure::bind($afterAll, null, self::class));
|
||||
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function expect($value): Expectation
|
||||
{
|
||||
return new Expectation($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed before the test.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
TestSuite::getInstance()->test = $this;
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
|
||||
|
||||
$this->__callClosure($beforeEach, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed after the test.
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
||||
|
||||
$this->__callClosure($afterEach, func_get_args());
|
||||
|
||||
parent::tearDown();
|
||||
|
||||
TestSuite::getInstance()->test = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test case as string.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s::%s',
|
||||
self::$__filename,
|
||||
$this->__description
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the test.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __test()
|
||||
{
|
||||
return $this->__callClosure($this->__test, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function __callClosure(Closure $closure, array $arguments)
|
||||
{
|
||||
return ExceptionTrace::ensure(function () use ($closure, $arguments) {
|
||||
return call_user_func_array(Closure::bind($closure, $this, get_class($this)), $arguments);
|
||||
});
|
||||
}
|
||||
|
||||
public function getPrintableTestCaseName(): string
|
||||
{
|
||||
return ltrim(self::class, 'P\\');
|
||||
}
|
||||
}
|
||||
307
src/Concerns/Testable.php
Normal file
307
src/Concerns/Testable.php
Normal file
@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\ExecutionOrderDependency;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* To avoid inheritance conflicts, all the fields related
|
||||
* to Pest only will be prefixed by double underscore.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait Testable
|
||||
{
|
||||
/**
|
||||
* The test case description. Contains the first
|
||||
* argument of global functions like `it` and `test`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $__description;
|
||||
|
||||
/**
|
||||
* Holds the test closure function.
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
private $__test;
|
||||
|
||||
/**
|
||||
* Holds a global/shared beforeEach ("set up") closure if one has been
|
||||
* defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
*/
|
||||
private $beforeEach = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared afterEach ("tear down") closure if one has been
|
||||
* defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
*/
|
||||
private $afterEach = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared beforeAll ("set up before") closure if one has been
|
||||
* defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
*/
|
||||
private static $beforeAll = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared afterAll ("tear down after") closure if one has
|
||||
* been defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
*/
|
||||
private static $afterAll = null;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the test case.
|
||||
*/
|
||||
public function __construct(Closure $test, string $description, array $data)
|
||||
{
|
||||
$this->__test = $test;
|
||||
$this->__description = $description;
|
||||
self::$beforeAll = null;
|
||||
self::$afterAll = null;
|
||||
|
||||
parent::__construct('__test', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the groups to the current test case.
|
||||
*/
|
||||
public function addGroups(array $groups): void
|
||||
{
|
||||
$groups = array_unique(array_merge($this->getGroups(), $groups));
|
||||
|
||||
$this->setGroups($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dependencies to the test case and map them to instances of ExecutionOrderDependency.
|
||||
*/
|
||||
public function addDependencies(array $tests): void
|
||||
{
|
||||
$className = get_class($this);
|
||||
|
||||
$tests = array_map(function (string $test) use ($className): ExecutionOrderDependency {
|
||||
if (strpos($test, '::') === false) {
|
||||
$test = "{$className}::{$test}";
|
||||
}
|
||||
|
||||
return new ExecutionOrderDependency($test, null, '');
|
||||
}, $tests);
|
||||
|
||||
$this->setDependencies($tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" before all test hook that will execute **before**
|
||||
* the test defined `beforeAll` hook(s).
|
||||
*/
|
||||
public function addBeforeAll(?Closure $hook): void
|
||||
{
|
||||
if (!$hook) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$beforeAll = (self::$beforeAll instanceof Closure)
|
||||
? ChainableClosure::fromStatic(self::$beforeAll, $hook)
|
||||
: $hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" after all test hook that will execute **before**
|
||||
* the test defined `afterAll` hook(s).
|
||||
*/
|
||||
public function addAfterAll(?Closure $hook): void
|
||||
{
|
||||
if (!$hook) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$afterAll = (self::$afterAll instanceof Closure)
|
||||
? ChainableClosure::fromStatic(self::$afterAll, $hook)
|
||||
: $hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" before each test hook that will execute **before**
|
||||
* the test defined `beforeEach` hook.
|
||||
*/
|
||||
public function addBeforeEach(?Closure $hook): void
|
||||
{
|
||||
$this->addHook('beforeEach', $hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" after each test hook that will execute **before**
|
||||
* the test defined `afterEach` hook.
|
||||
*/
|
||||
public function addAfterEach(?Closure $hook): void
|
||||
{
|
||||
$this->addHook('afterEach', $hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/global hook and compose them if more than one is passed.
|
||||
*/
|
||||
private function addHook(string $property, ?Closure $hook): void
|
||||
{
|
||||
if (!$hook) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->{$property} = ($this->{$property} instanceof Closure)
|
||||
? ChainableClosure::from($this->{$property}, $hook)
|
||||
: $hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test case name. Note that, in Pest
|
||||
* we ignore withDataset argument as the description
|
||||
* already contains the dataset description.
|
||||
*/
|
||||
public function getName(bool $withDataSet = true): string
|
||||
{
|
||||
return $this->__description;
|
||||
}
|
||||
|
||||
public static function __getFileName(): string
|
||||
{
|
||||
return self::$__filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before the first test of this test class is run.
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
|
||||
|
||||
if (self::$beforeAll instanceof Closure) {
|
||||
$beforeAll = ChainableClosure::fromStatic(self::$beforeAll, $beforeAll);
|
||||
}
|
||||
|
||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after the last test of this test class is run.
|
||||
*/
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
|
||||
|
||||
if (self::$afterAll instanceof Closure) {
|
||||
$afterAll = ChainableClosure::fromStatic(self::$afterAll, $afterAll);
|
||||
}
|
||||
|
||||
call_user_func(Closure::bind($afterAll, null, self::class));
|
||||
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed before the test.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
TestSuite::getInstance()->test = $this;
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
|
||||
|
||||
if ($this->beforeEach instanceof Closure) {
|
||||
$beforeEach = ChainableClosure::from($this->beforeEach, $beforeEach);
|
||||
}
|
||||
|
||||
$this->__callClosure($beforeEach, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed after the test.
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
||||
|
||||
if ($this->afterEach instanceof Closure) {
|
||||
$afterEach = ChainableClosure::from($this->afterEach, $afterEach);
|
||||
}
|
||||
|
||||
$this->__callClosure($afterEach, func_get_args());
|
||||
|
||||
parent::tearDown();
|
||||
|
||||
TestSuite::getInstance()->test = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test case as string.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s::%s',
|
||||
self::$__filename,
|
||||
$this->__description
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the test.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __test()
|
||||
{
|
||||
return $this->__callClosure($this->__test, $this->resolveTestArguments(func_get_args()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the passed arguments. Any Closures will be bound to the testcase and resolved.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function resolveTestArguments(array $arguments): array
|
||||
{
|
||||
return array_map(function ($data) {
|
||||
return $data instanceof Closure ? $this->__callClosure($data, []) : $data;
|
||||
}, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function __callClosure(Closure $closure, array $arguments)
|
||||
{
|
||||
return ExceptionTrace::ensure(function () use ($closure, $arguments) {
|
||||
return call_user_func_array(Closure::bind($closure, $this, get_class($this)), $arguments);
|
||||
});
|
||||
}
|
||||
|
||||
public function getPrintableTestCaseName(): string
|
||||
{
|
||||
return ltrim(self::class, 'P\\');
|
||||
}
|
||||
}
|
||||
@ -6,11 +6,11 @@ namespace Pest\Console;
|
||||
|
||||
use Pest\Actions\AddsDefaults;
|
||||
use Pest\Actions\AddsTests;
|
||||
use Pest\Actions\InteractsWithPlugins;
|
||||
use Pest\Actions\LoadStructure;
|
||||
use Pest\Actions\ValidatesConfiguration;
|
||||
use Pest\Contracts\Plugins\AddsOutput;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Plugin\Loader;
|
||||
use Pest\Plugins\Version;
|
||||
use Pest\Support\Container;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestSuite as BaseTestSuite;
|
||||
use PHPUnit\TextUI\Command as BaseCommand;
|
||||
@ -55,23 +55,12 @@ final class Command extends BaseCommand
|
||||
*/
|
||||
protected function handleArguments(array $argv): void
|
||||
{
|
||||
/*
|
||||
* First, let's call all plugins that want to handle arguments
|
||||
*/
|
||||
$plugins = Loader::getPlugins(HandlesArguments::class);
|
||||
$argv = InteractsWithPlugins::handleArguments($argv);
|
||||
|
||||
/** @var HandlesArguments $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$argv = $plugin->handleArguments($argv);
|
||||
}
|
||||
|
||||
/*
|
||||
* Next, as usual, let's send the console arguments to PHPUnit.
|
||||
*/
|
||||
parent::handleArguments($argv);
|
||||
|
||||
/*
|
||||
* Finally, let's validate the configuration. Making
|
||||
* Let's validate the configuration. Making
|
||||
* sure all options are yet supported by Pest.
|
||||
*/
|
||||
ValidatesConfiguration::in($this->arguments);
|
||||
@ -88,8 +77,6 @@ final class Command extends BaseCommand
|
||||
*/
|
||||
$this->arguments = AddsDefaults::to($this->arguments);
|
||||
|
||||
LoadStructure::in($this->testSuite->rootPath);
|
||||
|
||||
$testRunner = new TestRunner($this->arguments['loader']);
|
||||
$testSuite = $this->arguments['test'];
|
||||
|
||||
@ -125,18 +112,21 @@ final class Command extends BaseCommand
|
||||
*/
|
||||
public function run(array $argv, bool $exit = true): int
|
||||
{
|
||||
LoadStructure::in($this->testSuite->rootPath);
|
||||
|
||||
$result = parent::run($argv, false);
|
||||
|
||||
/*
|
||||
* Let's call all plugins that want to add output after test execution
|
||||
*/
|
||||
$plugins = Loader::getPlugins(AddsOutput::class);
|
||||
|
||||
/** @var AddsOutput $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$result = $plugin->addOutput($result);
|
||||
}
|
||||
$result = InteractsWithPlugins::addOutput($result);
|
||||
|
||||
exit($result);
|
||||
}
|
||||
|
||||
protected function showHelp(): void
|
||||
{
|
||||
/** @var Version $version */
|
||||
$version = Container::getInstance()->get(Version::class);
|
||||
$version->handleArguments(['--version']);
|
||||
parent::showHelp();
|
||||
|
||||
(new Help($this->output))();
|
||||
}
|
||||
}
|
||||
|
||||
37
src/Console/Help.php
Normal file
37
src/Console/Help.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Console;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Help
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
private const HELP_MESSAGES = [
|
||||
'<comment>Pest Options:</comment>',
|
||||
' <info>--init</info> Initialise a standard Pest configuration',
|
||||
' <info>--coverage</info> Enable coverage and output to standard output',
|
||||
' <info>--min=<fg=cyan><N></></info> Set the minimum required coverage percentage (<N>), and fail if not met',
|
||||
' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
|
||||
];
|
||||
|
||||
/** @var OutputInterface */
|
||||
private $output;
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function __invoke(): void
|
||||
{
|
||||
foreach (self::HELP_MESSAGES as $message) {
|
||||
$this->output->writeln($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Datasets.php
104
src/Datasets.php
@ -18,14 +18,14 @@ final class Datasets
|
||||
/**
|
||||
* Holds the datasets.
|
||||
*
|
||||
* @var array<string, \Closure|iterable<int, mixed>>
|
||||
* @var array<int|string, Closure|iterable<int|string, mixed>>
|
||||
*/
|
||||
private static $datasets = [];
|
||||
|
||||
/**
|
||||
* Sets the given.
|
||||
*
|
||||
* @param Closure|iterable<int, mixed> $data
|
||||
* @param Closure|iterable<int|string, mixed> $data
|
||||
*/
|
||||
public static function set(string $name, $data): void
|
||||
{
|
||||
@ -37,7 +37,7 @@ final class Datasets
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure|iterable<int, mixed>
|
||||
* @return Closure|iterable<int|string, mixed>
|
||||
*/
|
||||
public static function get(string $name)
|
||||
{
|
||||
@ -51,36 +51,34 @@ final class Datasets
|
||||
/**
|
||||
* Resolves the current dataset to an array value.
|
||||
*
|
||||
* @param Traversable<int, mixed>|Closure|iterable<int, mixed>|string|null $data
|
||||
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function resolve(string $description, $data): array
|
||||
public static function resolve(string $description, array $datasets): array
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (is_null($data) || empty($data)) {
|
||||
if (empty($datasets)) {
|
||||
return [$description => []];
|
||||
}
|
||||
|
||||
if (is_string($data)) {
|
||||
$data = self::get($data);
|
||||
}
|
||||
$datasets = self::processDatasets($datasets);
|
||||
|
||||
if (is_callable($data)) {
|
||||
$data = call_user_func($data);
|
||||
}
|
||||
|
||||
if ($data instanceof Traversable) {
|
||||
$data = iterator_to_array($data);
|
||||
}
|
||||
$datasetCombinations = self::getDataSetsCombinations($datasets);
|
||||
|
||||
$dataSetDescriptions = [];
|
||||
$dataSetValues = [];
|
||||
|
||||
foreach ($data as $values) {
|
||||
$values = is_array($values) ? $values : [$values];
|
||||
foreach ($datasetCombinations as $datasetCombination) {
|
||||
$partialDescriptions = [];
|
||||
$values = [];
|
||||
|
||||
$dataSetDescriptions[] = $description . self::getDataSetDescription($values);
|
||||
foreach ($datasetCombination as $dataset_data) {
|
||||
$partialDescriptions[] = $dataset_data['label'];
|
||||
$values = array_merge($values, $dataset_data['values']);
|
||||
}
|
||||
|
||||
$dataSetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions);
|
||||
$dataSetValues[] = $values;
|
||||
}
|
||||
|
||||
@ -104,12 +102,76 @@ final class Datasets
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
|
||||
*
|
||||
* @return array<array>
|
||||
*/
|
||||
private static function processDatasets(array $datasets): array
|
||||
{
|
||||
$processedDatasets = [];
|
||||
|
||||
foreach ($datasets as $index => $data) {
|
||||
$processedDataset = [];
|
||||
|
||||
if (is_string($data)) {
|
||||
$datasets[$index] = self::get($data);
|
||||
}
|
||||
|
||||
if (is_callable($datasets[$index])) {
|
||||
$datasets[$index] = call_user_func($datasets[$index]);
|
||||
}
|
||||
|
||||
if ($datasets[$index] instanceof Traversable) {
|
||||
$datasets[$index] = iterator_to_array($datasets[$index]);
|
||||
}
|
||||
|
||||
foreach ($datasets[$index] as $key => $values) {
|
||||
$values = is_array($values) ? $values : [$values];
|
||||
$processedDataset[] = [
|
||||
'label' => self::getDataSetDescription($key, $values),
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
|
||||
$processedDatasets[] = $processedDataset;
|
||||
}
|
||||
|
||||
return $processedDatasets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array> $combinations
|
||||
*
|
||||
* @return array<array>
|
||||
*/
|
||||
private static function getDataSetsCombinations(array $combinations): array
|
||||
{
|
||||
$result = [[]];
|
||||
foreach ($combinations as $index => $values) {
|
||||
$tmp = [];
|
||||
foreach ($result as $resultItem) {
|
||||
foreach ($values as $value) {
|
||||
$tmp[] = array_merge($resultItem, [$index => $value]);
|
||||
}
|
||||
}
|
||||
$result = $tmp;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $key
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
private static function getDataSetDescription(array $data): string
|
||||
private static function getDataSetDescription($key, array $data): string
|
||||
{
|
||||
$exporter = new Exporter();
|
||||
|
||||
return \sprintf(' with (%s)', $exporter->shortenedRecursiveExport($data));
|
||||
if (is_int($key)) {
|
||||
return \sprintf('(%s)', $exporter->shortenedRecursiveExport($data));
|
||||
}
|
||||
|
||||
return \sprintf('data set "%s"', $key);
|
||||
}
|
||||
}
|
||||
|
||||
77
src/Each.php
Normal file
77
src/Each.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @mixin Expectation
|
||||
*/
|
||||
final class Each
|
||||
{
|
||||
/**
|
||||
* @var Expectation
|
||||
*/
|
||||
private $original;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $opposite = false;
|
||||
|
||||
/**
|
||||
* Creates an expectation on each item of the iterable "value".
|
||||
*/
|
||||
public function __construct(Expectation $original)
|
||||
{
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function and($value): Expectation
|
||||
{
|
||||
return $this->original->and($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the opposite expectation for the value.
|
||||
*/
|
||||
public function not(): Each
|
||||
{
|
||||
$this->opposite = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically calls methods on the class with the given arguments on each item.
|
||||
*
|
||||
* @param array<int|string, mixed> $arguments
|
||||
*/
|
||||
public function __call(string $name, array $arguments): Each
|
||||
{
|
||||
foreach ($this->original->value as $item) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->opposite ? expect($item)->not()->$name(...$arguments) : expect($item)->$name(...$arguments);
|
||||
}
|
||||
|
||||
$this->opposite = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically calls methods on the class without any arguments on each item.
|
||||
*/
|
||||
public function __get(string $name): Each
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->$name();
|
||||
}
|
||||
}
|
||||
36
src/Exceptions/DatasetMissing.php
Normal file
36
src/Exceptions/DatasetMissing.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Exceptions;
|
||||
|
||||
use BadFunctionCallException;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* Creates a new instance of dataset is not present for test that has arguments.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Create new exception instance.
|
||||
*
|
||||
* @param array<string, string> $args A map of argument names to their typee
|
||||
*/
|
||||
public function __construct(string $file, string $name, array $args)
|
||||
{
|
||||
parent::__construct(sprintf(
|
||||
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
|
||||
$name,
|
||||
count($args),
|
||||
implode(', ', array_map(static function (string $arg, string $type): string {
|
||||
return sprintf('%s $%s', $type, $arg);
|
||||
}, array_keys($args), $args)),
|
||||
$file,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvalidUsesPath extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of invalid uses path.
|
||||
*/
|
||||
public function __construct(string $target)
|
||||
{
|
||||
parent::__construct(sprintf('The path `%s` is not valid.', $target));
|
||||
}
|
||||
}
|
||||
24
src/Exceptions/MissingDependency.php
Normal file
24
src/Exceptions/MissingDependency.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of missing dependency.
|
||||
*/
|
||||
public function __construct(string $feature, string $dependency)
|
||||
{
|
||||
parent::__construct(sprintf('The feature "%s" requires "%s".', $feature, $dependency));
|
||||
}
|
||||
}
|
||||
@ -4,15 +4,30 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Pest\Concerns\Extendable;
|
||||
use Pest\Concerns\RetrievesValues;
|
||||
use Pest\Support\Arr;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @property Expectation $not Creates the opposite expectation.
|
||||
* @property Each $each Creates an expectation on each element on the traversable value.
|
||||
*/
|
||||
final class Expectation
|
||||
{
|
||||
use Extendable {
|
||||
__call as __extendsCall;
|
||||
}
|
||||
use RetrievesValues;
|
||||
|
||||
/**
|
||||
* The expectation value.
|
||||
*
|
||||
@ -22,10 +37,19 @@ final class Expectation
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The exporter instance, if any.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var Exporter|null
|
||||
*/
|
||||
private $exporter;
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param TValue $value
|
||||
*/
|
||||
public function __construct($value)
|
||||
{
|
||||
@ -35,13 +59,56 @@ final class Expectation
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function and($value): Expectation
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation with the decoded JSON value.
|
||||
*/
|
||||
public function json(): Expectation
|
||||
{
|
||||
return $this->toBeJson()->and(json_decode($this->value, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the expectation value and end the script.
|
||||
*
|
||||
* @param mixed $arguments
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function dd(...$arguments): void
|
||||
{
|
||||
if (function_exists('dd')) {
|
||||
dd($this->value, ...$arguments);
|
||||
}
|
||||
|
||||
var_dump($this->value);
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the expectation value to Ray along with all given arguments.
|
||||
*
|
||||
* @param mixed $arguments
|
||||
*/
|
||||
public function ray(...$arguments): self
|
||||
{
|
||||
if (function_exists('ray')) {
|
||||
// @phpstan-ignore-next-line
|
||||
ray($this->value, ...$arguments);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the opposite expectation for the value.
|
||||
*/
|
||||
@ -50,6 +117,60 @@ final class Expectation
|
||||
return new OppositeExpectation($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an expectation on each item of the iterable "value".
|
||||
*/
|
||||
public function each(callable $callback = null): Each
|
||||
{
|
||||
if (!is_iterable($this->value)) {
|
||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||
}
|
||||
|
||||
if (is_callable($callback)) {
|
||||
foreach ($this->value as $item) {
|
||||
$callback(new self($item));
|
||||
}
|
||||
}
|
||||
|
||||
return new Each($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to specify a sequential set of expectations for each item in a iterable "value".
|
||||
*
|
||||
* @template TSequenceValue
|
||||
*
|
||||
* @param callable(self, self): void|TSequenceValue ...$callbacks
|
||||
*/
|
||||
public function sequence(...$callbacks): Expectation
|
||||
{
|
||||
if (!is_iterable($this->value)) {
|
||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||
}
|
||||
|
||||
$value = is_array($this->value) ? $this->value : iterator_to_array($this->value);
|
||||
$keys = array_keys($value);
|
||||
$values = array_values($value);
|
||||
|
||||
$index = 0;
|
||||
|
||||
while (count($callbacks) < count($values)) {
|
||||
$callbacks[] = $callbacks[$index];
|
||||
$index = $index < count($values) - 1 ? $index + 1 : 0;
|
||||
}
|
||||
|
||||
foreach ($values as $key => $item) {
|
||||
if (is_callable($callbacks[$key])) {
|
||||
call_user_func($callbacks[$key], new self($item), new self($keys[$key]));
|
||||
continue;
|
||||
}
|
||||
|
||||
(new self($item))->toEqual($callbacks[$key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that two variables have the same type and
|
||||
* value. Used on objects, it asserts that two
|
||||
@ -84,6 +205,16 @@ final class Expectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is truthy.
|
||||
*/
|
||||
public function toBeTruthy(): Expectation
|
||||
{
|
||||
Assert::assertTrue((bool) $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is false.
|
||||
*/
|
||||
@ -94,6 +225,16 @@ final class Expectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is falsy.
|
||||
*/
|
||||
public function toBeFalsy(): Expectation
|
||||
{
|
||||
Assert::assertFalse((bool) $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is greater than $expected.
|
||||
*
|
||||
@ -145,15 +286,37 @@ final class Expectation
|
||||
/**
|
||||
* Asserts that $needle is an element of the value.
|
||||
*
|
||||
* @param mixed $needle
|
||||
* @param mixed $needles
|
||||
*/
|
||||
public function toContain($needle): Expectation
|
||||
public function toContain(...$needles): Expectation
|
||||
{
|
||||
foreach ($needles as $needle) {
|
||||
if (is_string($this->value)) {
|
||||
Assert::assertStringContainsString($needle, $this->value);
|
||||
} else {
|
||||
Assert::assertContains($needle, $this->value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value starts with $expected.
|
||||
*/
|
||||
public function toStartWith(string $expected): Expectation
|
||||
{
|
||||
Assert::assertStringStartsWith($expected, $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value ends with $expected.
|
||||
*/
|
||||
public function toEndWith(string $expected): Expectation
|
||||
{
|
||||
Assert::assertStringEndsWith($expected, $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -170,11 +333,20 @@ final class Expectation
|
||||
|
||||
/**
|
||||
* Asserts that the value contains the property $name.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function toHaveProperty(string $name): Expectation
|
||||
public function toHaveProperty(string $name, $value = null): Expectation
|
||||
{
|
||||
$this->toBeObject();
|
||||
|
||||
Assert::assertTrue(property_exists($this->value, $name));
|
||||
|
||||
if (func_num_args() > 1) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
Assert::assertEquals($value, $this->value->{$name});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -221,6 +393,18 @@ final class Expectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is one of the given values.
|
||||
*
|
||||
* @param iterable<int|string, mixed> $values
|
||||
*/
|
||||
public function toBeIn(iterable $values): Expectation
|
||||
{
|
||||
Assert::assertContains($this->value, $values);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is infinite.
|
||||
*/
|
||||
@ -354,6 +538,17 @@ final class Expectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is a JSON string.
|
||||
*/
|
||||
public function toBeJson(): Expectation
|
||||
{
|
||||
Assert::assertIsString($this->value);
|
||||
Assert::assertJson($this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value is NAN.
|
||||
*/
|
||||
@ -376,10 +571,43 @@ final class Expectation
|
||||
|
||||
/**
|
||||
* Asserts that the value array has the provided $key.
|
||||
*
|
||||
* @param string|int $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function toHaveKey(string $key): Expectation
|
||||
public function toHaveKey($key, $value = null): Expectation
|
||||
{
|
||||
Assert::assertArrayHasKey($key, $this->value);
|
||||
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
|
||||
$array = $this->value->toArray();
|
||||
} else {
|
||||
$array = (array) $this->value;
|
||||
}
|
||||
|
||||
try {
|
||||
Assert::assertTrue(Arr::has($array, $key));
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
} catch (ExpectationFailedException $exception) {
|
||||
throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure());
|
||||
}
|
||||
|
||||
if (func_num_args() > 1) {
|
||||
Assert::assertEquals($value, Arr::get($array, $key));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value array has the provided $keys.
|
||||
*
|
||||
* @param array<int, int|string> $keys
|
||||
*/
|
||||
public function toHaveKeys(array $keys): Expectation
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
$this->toHaveKey($key);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -445,12 +673,126 @@ final class Expectation
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically calls methods on the class without any arguments.
|
||||
* Asserts that the value array matches the given array subset.
|
||||
*
|
||||
* @return Expectation
|
||||
* @param array<int|string, mixed> $array
|
||||
*/
|
||||
public function toMatchArray($array): Expectation
|
||||
{
|
||||
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
|
||||
$valueAsArray = $this->value->toArray();
|
||||
} else {
|
||||
$valueAsArray = (array) $this->value;
|
||||
}
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
Assert::assertArrayHasKey($key, $valueAsArray);
|
||||
|
||||
Assert::assertEquals(
|
||||
$value,
|
||||
$valueAsArray[$key],
|
||||
sprintf(
|
||||
'Failed asserting that an array has a key %s with the value %s.',
|
||||
$this->export($key),
|
||||
$this->export($valueAsArray[$key]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value object matches a subset
|
||||
* of the properties of an given object.
|
||||
*
|
||||
* @param array<string, mixed>|object $object
|
||||
*/
|
||||
public function toMatchObject($object): Expectation
|
||||
{
|
||||
foreach ((array) $object as $property => $value) {
|
||||
Assert::assertTrue(property_exists($this->value, $property));
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
$propertyValue = $this->value->{$property};
|
||||
Assert::assertEquals(
|
||||
$value,
|
||||
$propertyValue,
|
||||
sprintf(
|
||||
'Failed asserting that an object has a property %s with the value %s.',
|
||||
$this->export($property),
|
||||
$this->export($propertyValue),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value matches a regular expression.
|
||||
*/
|
||||
public function toMatch(string $expression): Expectation
|
||||
{
|
||||
Assert::assertMatchesRegularExpression($expression, $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value matches a constraint.
|
||||
*/
|
||||
public function toMatchConstraint(Constraint $constraint): Expectation
|
||||
{
|
||||
Assert::assertThat($this->value, $constraint);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the given value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function export($value): string
|
||||
{
|
||||
if ($this->exporter === null) {
|
||||
$this->exporter = new Exporter();
|
||||
}
|
||||
|
||||
return $this->exporter->export($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically handle calls to the class or
|
||||
* creates a new higher order expectation.
|
||||
*
|
||||
* @param array<int, mixed> $parameters
|
||||
*
|
||||
* @return HigherOrderExpectation|mixed
|
||||
*/
|
||||
public function __call(string $method, array $parameters)
|
||||
{
|
||||
if (!static::hasExtend($method)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
return new HigherOrderExpectation($this, $this->value->$method(...$parameters));
|
||||
}
|
||||
|
||||
return $this->__extendsCall($method, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically calls methods on the class without any arguments
|
||||
* or creates a new higher order expectation.
|
||||
*
|
||||
* @return Expectation|HigherOrderExpectation
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
if (!method_exists($this, $name) && !static::hasExtend($name)) {
|
||||
return new HigherOrderExpectation($this, $this->retrieve($name, $this->value));
|
||||
}
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->{$name}();
|
||||
}
|
||||
|
||||
@ -5,14 +5,17 @@ declare(strict_types=1);
|
||||
namespace Pest\Factories;
|
||||
|
||||
use Closure;
|
||||
use ParseError;
|
||||
use Pest\Concerns;
|
||||
use Pest\Contracts\HasPrintableTestCaseName;
|
||||
use Pest\Datasets;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Support\HigherOrderMessageCollection;
|
||||
use Pest\Support\NullClosure;
|
||||
use Pest\Support\Str;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -59,9 +62,9 @@ final class TestCaseFactory
|
||||
/**
|
||||
* Holds the dataset, if any.
|
||||
*
|
||||
* @var Closure|iterable<int, mixed>|string|null
|
||||
* @var array<Closure|iterable<int|string, mixed>|string>
|
||||
*/
|
||||
public $dataset;
|
||||
public $datasets = [];
|
||||
|
||||
/**
|
||||
* The FQN of the test case class.
|
||||
@ -76,7 +79,8 @@ final class TestCaseFactory
|
||||
* @var array <int, string>
|
||||
*/
|
||||
public $traits = [
|
||||
Concerns\TestCase::class,
|
||||
Concerns\Testable::class,
|
||||
Concerns\Expectable::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@ -110,7 +114,11 @@ final class TestCaseFactory
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->description = $description;
|
||||
$this->test = $closure ?? NullClosure::create();
|
||||
$this->test = $closure ?? function (): void {
|
||||
if (Assert::getCount() === 0) {
|
||||
self::markTestIncomplete(); // @phpstan-ignore-line
|
||||
}
|
||||
};
|
||||
|
||||
$this->factoryProxies = new HigherOrderMessageCollection();
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
@ -139,6 +147,7 @@ final class TestCaseFactory
|
||||
$proxies->proxy($this);
|
||||
$chains->chain($this);
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
return call_user_func(Closure::bind($factoryTest, $this, get_class($this)), ...func_get_args());
|
||||
};
|
||||
|
||||
@ -151,19 +160,11 @@ final class TestCaseFactory
|
||||
return $testCase;
|
||||
};
|
||||
|
||||
$datasets = Datasets::resolve($this->description, $this->dataset);
|
||||
$datasets = Datasets::resolve($this->description, $this->datasets);
|
||||
|
||||
return array_map($createTest, array_keys($datasets), $datasets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fully qualified class name from the current filename.
|
||||
*/
|
||||
public function getClassName(): string
|
||||
{
|
||||
return $this->makeClassFromFilename($this->filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fully qualified class name from the given filename.
|
||||
*/
|
||||
@ -176,7 +177,7 @@ final class TestCaseFactory
|
||||
}, $filename);
|
||||
}
|
||||
|
||||
$filename = (string) realpath($filename);
|
||||
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
|
||||
$rootPath = TestSuite::getInstance()->rootPath;
|
||||
$relativePath = str_replace($rootPath . DIRECTORY_SEPARATOR, '', $filename);
|
||||
$relativePath = dirname(ucfirst($relativePath)) . DIRECTORY_SEPARATOR . basename($relativePath, '.php');
|
||||
@ -184,8 +185,12 @@ final class TestCaseFactory
|
||||
|
||||
// Strip out any %-encoded octets.
|
||||
$relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath);
|
||||
// Remove escaped quote sequences (maintain namespace)
|
||||
$relativePath = str_replace(array_map(function (string $quote): string {
|
||||
return sprintf('\\%s', $quote);
|
||||
}, ['\'', '"']), '', $relativePath);
|
||||
// Limit to A-Z, a-z, 0-9, '_', '-'.
|
||||
$relativePath = (string) preg_replace('/[^A-Za-z0-9.\\\]/', '', $relativePath);
|
||||
$relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath);
|
||||
|
||||
$classFQN = 'P\\' . $relativePath;
|
||||
if (class_exists($classFQN)) {
|
||||
@ -202,6 +207,12 @@ final class TestCaseFactory
|
||||
$namespace = implode('\\', $partsFQN);
|
||||
$baseClass = sprintf('\%s', $this->class);
|
||||
|
||||
if ('' === trim($className)) {
|
||||
$className = 'InvalidTestName' . Str::random();
|
||||
$classFQN .= $className;
|
||||
}
|
||||
|
||||
try {
|
||||
eval("
|
||||
namespace $namespace;
|
||||
|
||||
@ -211,7 +222,19 @@ final class TestCaseFactory
|
||||
private static \$__filename = '$filename';
|
||||
}
|
||||
");
|
||||
} catch (ParseError $caught) {
|
||||
throw new RuntimeException(sprintf('Unable to create test case for test file at %s', $filename), 1, $caught);
|
||||
}
|
||||
|
||||
return $classFQN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test case will receive argument input from Pest, or not.
|
||||
*/
|
||||
public function receivesArguments(): bool
|
||||
{
|
||||
return count($this->datasets) > 0
|
||||
|| $this->factoryProxies->count('addDependencies') > 0;
|
||||
}
|
||||
}
|
||||
|
||||
140
src/Functions.php
Normal file
140
src/Functions.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Pest\Datasets;
|
||||
use Pest\Expectation;
|
||||
use Pest\PendingObjects\AfterEachCall;
|
||||
use Pest\PendingObjects\BeforeEachCall;
|
||||
use Pest\PendingObjects\TestCall;
|
||||
use Pest\PendingObjects\UsesCall;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\Extendable;
|
||||
use Pest\Support\HigherOrderTapProxy;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value the Value
|
||||
*
|
||||
* @return Expectation|Extendable
|
||||
*/
|
||||
function expect($value = null)
|
||||
{
|
||||
if (func_num_args() === 0) {
|
||||
return new Extendable(Expectation::class);
|
||||
}
|
||||
|
||||
return new Expectation($value);
|
||||
}
|
||||
|
||||
if (!function_exists('beforeAll')) {
|
||||
/**
|
||||
* Runs the given closure before all tests in the current file.
|
||||
*/
|
||||
function beforeAll(Closure $closure): void
|
||||
{
|
||||
TestSuite::getInstance()->beforeAll->set($closure);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('beforeEach')) {
|
||||
/**
|
||||
* Runs the given closure before each test in the current file.
|
||||
*
|
||||
* @return BeforeEachCall|TestCase|mixed
|
||||
*/
|
||||
function beforeEach(Closure $closure = null): BeforeEachCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('dataset')) {
|
||||
/**
|
||||
* Registers the given dataset.
|
||||
*
|
||||
* @param Closure|iterable<int|string, mixed> $dataset
|
||||
*/
|
||||
function dataset(string $name, $dataset): void
|
||||
{
|
||||
Datasets::set($name, $dataset);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('uses')) {
|
||||
/**
|
||||
* The uses function binds the given
|
||||
* arguments to test closures.
|
||||
*/
|
||||
function uses(string ...$classAndTraits): UsesCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new UsesCall($filename, $classAndTraits);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('test')) {
|
||||
/**
|
||||
* Adds the given closure as a test. The first argument
|
||||
* is the test description; the second argument is
|
||||
* a closure that contains the test expectations.
|
||||
*
|
||||
* @return TestCall|TestCase|mixed
|
||||
*/
|
||||
function test(string $description = null, Closure $closure = null)
|
||||
{
|
||||
if ($description === null && TestSuite::getInstance()->test !== null) {
|
||||
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
|
||||
}
|
||||
|
||||
$filename = Backtrace::testFile();
|
||||
|
||||
return new TestCall(TestSuite::getInstance(), $filename, $description, $closure);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('it')) {
|
||||
/**
|
||||
* Adds the given closure as a test. The first argument
|
||||
* is the test description; the second argument is
|
||||
* a closure that contains the test expectations.
|
||||
*
|
||||
* @return TestCall|TestCase|mixed
|
||||
*/
|
||||
function it(string $description, Closure $closure = null): TestCall
|
||||
{
|
||||
$description = sprintf('it %s', $description);
|
||||
|
||||
return test($description, $closure);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('afterEach')) {
|
||||
/**
|
||||
* Runs the given closure after each test in the current file.
|
||||
*
|
||||
* @return AfterEachCall|TestCase|mixed
|
||||
*/
|
||||
function afterEach(Closure $closure = null): AfterEachCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new AfterEachCall(TestSuite::getInstance(), $filename, $closure);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('afterAll')) {
|
||||
/**
|
||||
* Runs the given closure after all tests in the current file.
|
||||
*/
|
||||
function afterAll(Closure $closure): void
|
||||
{
|
||||
TestSuite::getInstance()->afterAll->set($closure);
|
||||
}
|
||||
}
|
||||
144
src/HigherOrderExpectation.php
Normal file
144
src/HigherOrderExpectation.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use Pest\Concerns\Expectable;
|
||||
use Pest\Concerns\RetrievesValues;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @mixin Expectation
|
||||
*/
|
||||
final class HigherOrderExpectation
|
||||
{
|
||||
use Expectable;
|
||||
use RetrievesValues;
|
||||
|
||||
/**
|
||||
* @var Expectation
|
||||
*/
|
||||
private $original;
|
||||
|
||||
/**
|
||||
* @var Expectation|Each
|
||||
*/
|
||||
private $expectation;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $opposite = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $shouldReset = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* Creates a new higher order expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(Expectation $original, $value)
|
||||
{
|
||||
$this->original = $original;
|
||||
$this->expectation = $this->expect($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the opposite expectation for the value.
|
||||
*/
|
||||
public function not(): HigherOrderExpectation
|
||||
{
|
||||
$this->opposite = !$this->opposite;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @param TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function and($value): Expectation
|
||||
{
|
||||
return $this->expect($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically calls methods on the class with the given arguments.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __call(string $name, array $arguments): self
|
||||
{
|
||||
if (!$this->expectationHasMethod($name)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
return new self($this->original, $this->getValue()->$name(...$arguments));
|
||||
}
|
||||
|
||||
return $this->performAssertion($name, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses properties in the value or in the expectation.
|
||||
*/
|
||||
public function __get(string $name): self
|
||||
{
|
||||
if ($name === 'not') {
|
||||
return $this->not();
|
||||
}
|
||||
|
||||
if (!$this->expectationHasMethod($name)) {
|
||||
return new self($this->original, $this->retrieve($name, $this->getValue()));
|
||||
}
|
||||
|
||||
return $this->performAssertion($name, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the original expectation has the given method name.
|
||||
*/
|
||||
private function expectationHasMethod(string $name): bool
|
||||
{
|
||||
return method_exists($this->original, $name) || $this->original::hasExtend($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the applicable value based on the current reset condition.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function getValue()
|
||||
{
|
||||
return $this->shouldReset ? $this->original->value : $this->expectation->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the given assertion with the current expectation.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
private function performAssertion(string $name, array $arguments): self
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->expectation = ($this->opposite ? $this->expectation->not() : $this->expectation)->{$name}(...$arguments);
|
||||
|
||||
$this->opposite = false;
|
||||
$this->shouldReset = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,8 @@ use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
use Pest\Exceptions\InvalidConsoleArgument;
|
||||
use function Pest\testDirectory;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -19,7 +21,8 @@ final class PestDatasetCommand extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'pest:dataset {name : The name of the dataset}';
|
||||
protected $signature = 'pest:dataset {name : The name of the dataset}
|
||||
{--test-directory=tests : The name of the tests directory}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -33,10 +36,13 @@ final class PestDatasetCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
TestSuite::getInstance(base_path(), $this->option('test-directory'));
|
||||
|
||||
/** @var string $name */
|
||||
$name = $this->argument('name');
|
||||
|
||||
$relativePath = sprintf('tests/Datasets/%s.php', ucfirst($name));
|
||||
$relativePath = sprintf(testDirectory('Datasets/%s.php'), ucfirst($name));
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
$target = base_path($relativePath);
|
||||
|
||||
43
src/Laravel/Commands/PestDuskCommand.php
Normal file
43
src/Laravel/Commands/PestDuskCommand.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Laravel\Commands;
|
||||
|
||||
use Laravel\Dusk\Console\DuskCommand;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class PestDuskCommand extends DuskCommand
|
||||
{
|
||||
/**
|
||||
* The console command name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'pest:dusk
|
||||
{--browse : Open a browser instead of using headless mode}
|
||||
{--without-tty : Disable output to TTY}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Run the Dusk tests for the application with Pest';
|
||||
|
||||
/**
|
||||
* Get the PHP binary to execute.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function binary()
|
||||
{
|
||||
if ('phpdbg' === PHP_SAPI) {
|
||||
return [PHP_BINARY, '-qrr', 'vendor/pestphp/pest/bin/pest'];
|
||||
}
|
||||
|
||||
return [PHP_BINARY, 'vendor/pestphp/pest/bin/pest'];
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,8 @@ use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Pest\Console\Thanks;
|
||||
use Pest\Exceptions\InvalidConsoleArgument;
|
||||
use Pest\Support\Str;
|
||||
use function Pest\testDirectory;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -20,7 +21,7 @@ final class PestInstallCommand extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'pest:install';
|
||||
protected $signature = 'pest:install {--test-directory=tests : The name of the tests directory}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -35,15 +36,14 @@ final class PestInstallCommand extends Command
|
||||
public function handle(): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$pest = base_path('tests/Pest.php');
|
||||
/* @phpstan-ignore-next-line */
|
||||
$helpers = base_path('tests/Helpers.php');
|
||||
$stubs = $this->isLumen() ? 'stubs/Lumen' : 'stubs/Laravel';
|
||||
TestSuite::getInstance(base_path(), $this->option('test-directory'));
|
||||
|
||||
foreach ([$pest, $helpers] as $file) {
|
||||
if (File::exists($file)) {
|
||||
throw new InvalidConsoleArgument(sprintf('%s already exist', $file));
|
||||
}
|
||||
/* @phpstan-ignore-next-line */
|
||||
$pest = base_path(testDirectory('Pest.php'));
|
||||
$stubs = 'stubs/Laravel';
|
||||
|
||||
if (File::exists($pest)) {
|
||||
throw new InvalidConsoleArgument(sprintf('%s already exist', $pest));
|
||||
}
|
||||
|
||||
File::copy(implode(DIRECTORY_SEPARATOR, [
|
||||
@ -52,26 +52,10 @@ final class PestInstallCommand extends Command
|
||||
'Pest.php',
|
||||
]), $pest);
|
||||
|
||||
File::copy(implode(DIRECTORY_SEPARATOR, [
|
||||
dirname(__DIR__, 3),
|
||||
$stubs,
|
||||
'Helpers.php',
|
||||
]), $helpers);
|
||||
|
||||
$this->output->success('`tests/Pest.php` created successfully.');
|
||||
$this->output->success('`tests/Helpers.php` created successfully.');
|
||||
|
||||
if (!(bool) $this->option('no-interaction')) {
|
||||
(new Thanks($this->output))();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is a Lumen application.
|
||||
*/
|
||||
private function isLumen(): bool
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
return Str::startsWith(app()->version(), 'Lumen');
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Pest\Exceptions\InvalidConsoleArgument;
|
||||
use Pest\Support\Str;
|
||||
use function Pest\testDirectory;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -19,7 +21,7 @@ final class PestTestCommand extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'pest:test {name : The name of the file} {--unit : Create a unit test}';
|
||||
protected $signature = 'pest:test {name : The name of the file} {--unit : Create a unit test} {--dusk : Create a Dusk test} {--test-directory=tests : The name of the tests directory} {--force : Overwrite the existing test file with the same name}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -33,12 +35,16 @@ final class PestTestCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
TestSuite::getInstance(base_path(), $this->option('test-directory'));
|
||||
|
||||
/** @var string $name */
|
||||
$name = $this->argument('name');
|
||||
|
||||
$type = ((bool) $this->option('unit')) ? 'Unit' : 'Feature';
|
||||
$type = ((bool) $this->option('unit')) ? 'Unit' : (((bool) $this->option('dusk')) ? 'Browser' : 'Feature');
|
||||
|
||||
$relativePath = sprintf('tests/%s/%s.php',
|
||||
$relativePath = sprintf(
|
||||
testDirectory('%s/%s.php'),
|
||||
$type,
|
||||
ucfirst($name)
|
||||
);
|
||||
@ -50,7 +56,7 @@ final class PestTestCommand extends Command
|
||||
File::makeDirectory(dirname($target), 0777, true, true);
|
||||
}
|
||||
|
||||
if (File::exists($target)) {
|
||||
if (File::exists($target) && !(bool) $this->option('force')) {
|
||||
throw new InvalidConsoleArgument(sprintf('%s already exist', $target));
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,9 @@ declare(strict_types=1);
|
||||
namespace Pest\Laravel;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Dusk\Console\DuskCommand;
|
||||
use Pest\Laravel\Commands\PestDatasetCommand;
|
||||
use Pest\Laravel\Commands\PestDuskCommand;
|
||||
use Pest\Laravel\Commands\PestInstallCommand;
|
||||
use Pest\Laravel\Commands\PestTestCommand;
|
||||
|
||||
@ -22,6 +24,12 @@ final class PestServiceProvider extends ServiceProvider
|
||||
PestTestCommand::class,
|
||||
PestDatasetCommand::class,
|
||||
]);
|
||||
|
||||
if (class_exists(DuskCommand::class)) {
|
||||
$this->commands([
|
||||
PestDuskCommand::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
430
src/Logging/JUnit.php
Normal file
430
src/Logging/JUnit.php
Normal file
@ -0,0 +1,430 @@
|
||||
<?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 Pest\Logging;
|
||||
|
||||
use function class_exists;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Exception;
|
||||
use function get_class;
|
||||
use function method_exists;
|
||||
use Pest\Concerns\Testable;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\ExceptionWrapper;
|
||||
use PHPUnit\Framework\SelfDescribing;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestFailure;
|
||||
use PHPUnit\Framework\TestListener;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\Util\Filter;
|
||||
use PHPUnit\Util\Printer;
|
||||
use PHPUnit\Util\Xml;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use Throwable;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class JUnit extends Printer implements TestListener
|
||||
{
|
||||
/**
|
||||
* @var DOMDocument
|
||||
*/
|
||||
private $document;
|
||||
|
||||
/**
|
||||
* @var DOMElement
|
||||
*/
|
||||
private $root;
|
||||
|
||||
/**
|
||||
* @var DOMElement[]
|
||||
*/
|
||||
private $testSuites = [];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteTests = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteAssertions = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteErrors = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteWarnings = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteFailures = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteSkipped = [0];
|
||||
|
||||
/**
|
||||
* @var int[]|float[]
|
||||
*/
|
||||
private $testSuiteTimes = [0];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $testSuiteLevel = 0;
|
||||
|
||||
/**
|
||||
* @var DOMElement|null
|
||||
*/
|
||||
private $currentTestCase;
|
||||
|
||||
public function __construct(string $out)
|
||||
{
|
||||
$this->document = new DOMDocument('1.0', 'UTF-8');
|
||||
$this->document->formatOutput = true;
|
||||
|
||||
$this->root = $this->document->createElement('testsuites');
|
||||
$this->document->appendChild($this->root);
|
||||
|
||||
parent::__construct($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush buffer and close output.
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
$this->write($this->getXML());
|
||||
|
||||
parent::flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* An error occurred.
|
||||
*/
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $t, 'error');
|
||||
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning occurred.
|
||||
*/
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $e, 'warning');
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* A failure occurred.
|
||||
*/
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $e, 'failure');
|
||||
$this->testSuiteFailures[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incomplete test.
|
||||
*/
|
||||
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* Risky test.
|
||||
*/
|
||||
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Skipped test.
|
||||
*/
|
||||
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddSkipped();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$testSuite = $this->document->createElement('testsuite');
|
||||
$testSuite->setAttribute('name', $suite->getName());
|
||||
|
||||
if (class_exists($suite->getName(), false)) {
|
||||
try {
|
||||
$class = new ReflectionClass($suite->getName());
|
||||
|
||||
if ($class->hasMethod('__getFileName')) {
|
||||
$fileName = $class->getMethod('__getFileName')->invoke(null);
|
||||
} else {
|
||||
$fileName = $class->getFileName();
|
||||
}
|
||||
|
||||
$testSuite->setAttribute('file', $fileName);
|
||||
} catch (ReflectionException $e) {
|
||||
// @ignoreException
|
||||
}
|
||||
}
|
||||
|
||||
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->testSuiteWarnings[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): 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(
|
||||
'warnings',
|
||||
(string) $this->testSuiteWarnings[$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->testSuiteWarnings[$this->testSuiteLevel - 1] += $this->testSuiteWarnings[$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--;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test started.
|
||||
*
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
$usesDataprovider = false;
|
||||
|
||||
if (method_exists($test, 'usesDataProvider')) {
|
||||
$usesDataprovider = $test->usesDataProvider();
|
||||
}
|
||||
|
||||
$testCase = $this->document->createElement('testcase');
|
||||
$testCase->setAttribute('name', $test->getName());
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($test);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
// @phpstan-ignore-next-line
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$methodName = $test->getName(!$usesDataprovider);
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
try {
|
||||
$method = $class->getMethod($methodName);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
// @phpstan-ignore-next-line
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$testCase->setAttribute('class', $class->getName());
|
||||
$testCase->setAttribute('classname', str_replace('\\', '.', $class->getName()));
|
||||
$fileName = $class->getFileName();
|
||||
if ($fileName !== false) {
|
||||
$testCase->setAttribute('file', $fileName);
|
||||
}
|
||||
$testCase->setAttribute('line', (string) $method->getStartLine());
|
||||
}
|
||||
|
||||
if (TeamCity::isPestTest($test)) {
|
||||
$testCase->setAttribute('class', $test->getPrintableTestCaseName());
|
||||
$testCase->setAttribute('classname', str_replace('\\', '.', $test->getPrintableTestCaseName()));
|
||||
// @phpstan-ignore-next-line
|
||||
$testCase->setAttribute('file', $test->__getFileName());
|
||||
}
|
||||
|
||||
$this->currentTestCase = $testCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test ended.
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
$numAssertions = 0;
|
||||
|
||||
if (method_exists($test, 'getNumAssertions')) {
|
||||
$numAssertions = $test->getNumAssertions();
|
||||
}
|
||||
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
|
||||
|
||||
if ($this->currentTestCase !== null) {
|
||||
$this->currentTestCase->setAttribute(
|
||||
'assertions',
|
||||
(string) $numAssertions
|
||||
);
|
||||
|
||||
$this->currentTestCase->setAttribute(
|
||||
'time',
|
||||
sprintf('%F', $time)
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->appendChild(
|
||||
$this->currentTestCase
|
||||
);
|
||||
}
|
||||
|
||||
$this->testSuiteTests[$this->testSuiteLevel]++;
|
||||
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
|
||||
|
||||
$testOutput = '';
|
||||
|
||||
if (method_exists($test, 'hasOutput') && method_exists($test, 'getActualOutput')) {
|
||||
$testOutput = $test->hasOutput() ? $test->getActualOutput() : '';
|
||||
}
|
||||
|
||||
if ($testOutput !== '') {
|
||||
$systemOut = $this->document->createElement(
|
||||
'system-out',
|
||||
Xml::prepareString($testOutput)
|
||||
);
|
||||
|
||||
if ($this->currentTestCase !== null) {
|
||||
$this->currentTestCase->appendChild($systemOut);
|
||||
}
|
||||
}
|
||||
|
||||
$this->currentTestCase = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XML as a string.
|
||||
*/
|
||||
public function getXML(): string
|
||||
{
|
||||
$xml = $this->document->saveXML();
|
||||
if ($xml === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function doAddFault(Test $test, Throwable $t, string $type): void
|
||||
{
|
||||
if ($this->currentTestCase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($test instanceof SelfDescribing) {
|
||||
$buffer = $test->toString() . "\n";
|
||||
} else {
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
$buffer .= trim(
|
||||
TestFailure::exceptionToString($t) . "\n" .
|
||||
Filter::getFilteredStacktrace($t)
|
||||
);
|
||||
|
||||
$fault = $this->document->createElement(
|
||||
$type,
|
||||
Xml::prepareString($buffer)
|
||||
);
|
||||
|
||||
if ($t instanceof ExceptionWrapper) {
|
||||
$fault->setAttribute('type', $t->getClassName());
|
||||
} else {
|
||||
$fault->setAttribute('type', get_class($t));
|
||||
}
|
||||
|
||||
$this->currentTestCase->appendChild($fault);
|
||||
}
|
||||
|
||||
private function doAddSkipped(): void
|
||||
{
|
||||
if ($this->currentTestCase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$skipped = $this->document->createElement('skipped');
|
||||
|
||||
$this->currentTestCase->appendChild($skipped);
|
||||
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel]++;
|
||||
}
|
||||
}
|
||||
297
src/Logging/TeamCity.php
Normal file
297
src/Logging/TeamCity.php
Normal file
@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Logging;
|
||||
|
||||
use function getmypid;
|
||||
use Pest\Concerns\Logging\WritesToConsole;
|
||||
use Pest\Concerns\Testable;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use function Pest\version;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestResult;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
use function round;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use Throwable;
|
||||
|
||||
final class TeamCity extends DefaultResultPrinter
|
||||
{
|
||||
use WritesToConsole;
|
||||
private const PROTOCOL = 'pest_qn://';
|
||||
private const NAME = 'name';
|
||||
private const LOCATION_HINT = 'locationHint';
|
||||
private const DURATION = 'duration';
|
||||
private const TEST_SUITE_STARTED = 'testSuiteStarted';
|
||||
private const TEST_SUITE_FINISHED = 'testSuiteFinished';
|
||||
private const TEST_COUNT = 'testCount';
|
||||
private const TEST_STARTED = 'testStarted';
|
||||
private const TEST_FINISHED = 'testFinished';
|
||||
|
||||
/** @var int */
|
||||
private $flowId;
|
||||
|
||||
/** @var bool */
|
||||
private $isSummaryTestCountPrinted = false;
|
||||
|
||||
/** @var \PHPUnit\Util\Log\TeamCity */
|
||||
private $phpunitTeamCity;
|
||||
|
||||
/**
|
||||
* @param resource|string|null $out
|
||||
*/
|
||||
public function __construct($out, bool $verbose, string $colors)
|
||||
{
|
||||
parent::__construct($out, $verbose, $colors);
|
||||
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity($out, $verbose, $colors);
|
||||
|
||||
$this->logo();
|
||||
}
|
||||
|
||||
private function logo(): void
|
||||
{
|
||||
$this->writeNewLine();
|
||||
$this->write('Pest ' . version());
|
||||
$this->writeNewLine();
|
||||
}
|
||||
|
||||
public function printResult(TestResult $result): void
|
||||
{
|
||||
$this->write('Tests: ');
|
||||
|
||||
$results = [
|
||||
'failed' => ['count' => $result->errorCount() + $result->failureCount(), 'color' => 'fg-red'],
|
||||
'skipped' => ['count' => $result->skippedCount(), 'color' => 'fg-yellow'],
|
||||
'warned' => ['count' => $result->warningCount(), 'color' => 'fg-yellow'],
|
||||
'risked' => ['count' => $result->riskyCount(), 'color' => 'fg-yellow'],
|
||||
'incomplete' => ['count' => $result->notImplementedCount(), 'color' => 'fg-yellow'],
|
||||
'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'],
|
||||
];
|
||||
|
||||
$filteredResults = array_filter($results, function ($item): bool {
|
||||
return $item['count'] > 0;
|
||||
});
|
||||
|
||||
foreach ($filteredResults as $key => $info) {
|
||||
$this->writeWithColor($info['color'], $info['count'] . " $key", false);
|
||||
|
||||
if ($key !== array_reverse(array_keys($filteredResults))[0]) {
|
||||
$this->write(', ');
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->write("Assertions: $this->numAssertions");
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->write("Time: {$result->time()}s");
|
||||
|
||||
$this->writeNewLine();
|
||||
}
|
||||
|
||||
private function successfulTestCount(TestResult $result): int
|
||||
{
|
||||
return $result->count()
|
||||
- $result->failureCount()
|
||||
- $result->errorCount()
|
||||
- $result->skippedCount()
|
||||
- $result->warningCount()
|
||||
- $result->notImplementedCount()
|
||||
- $result->riskyCount();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
if (static::isCompoundTestSuite($suite)) {
|
||||
$this->writeWithColor('bold', ' ' . $suiteName);
|
||||
} elseif (static::isPestTestSuite($suite)) {
|
||||
$this->writeWithColor('fg-white, bold', ' ' . substr_replace($suiteName, '', 0, 2) . ' ');
|
||||
} else {
|
||||
$this->writeWithColor('fg-white, bold', ' ' . $suiteName);
|
||||
}
|
||||
|
||||
$this->writeNewLine();
|
||||
|
||||
$this->flowId = (int) getmypid();
|
||||
|
||||
if (!$this->isSummaryTestCountPrinted) {
|
||||
$this->printEvent(self::TEST_COUNT, [
|
||||
'count' => $suite->count(),
|
||||
]);
|
||||
$this->isSummaryTestCountPrinted = true;
|
||||
}
|
||||
|
||||
$this->printEvent(self::TEST_SUITE_STARTED, [
|
||||
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|int> $params
|
||||
*/
|
||||
private function printEvent(string $eventName, array $params = []): void
|
||||
{
|
||||
$this->write("##teamcity[{$eventName}");
|
||||
|
||||
if ($this->flowId !== 0) {
|
||||
$params['flowId'] = $this->flowId;
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$escapedValue = self::escapeValue((string) $value);
|
||||
$this->write(" {$key}='{$escapedValue}'");
|
||||
}
|
||||
|
||||
$this->write("]\n");
|
||||
}
|
||||
|
||||
private static function escapeValue(string $text): string
|
||||
{
|
||||
return str_replace(
|
||||
['|', "'", "\n", "\r", ']', '['],
|
||||
['||', "|'", '|n', '|r', '|]', '|['],
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->writeNewLine();
|
||||
|
||||
$this->printEvent(self::TEST_SUITE_FINISHED, [
|
||||
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->startTest($test);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent(self::TEST_STARTED, [
|
||||
self::NAME => $test->getName(),
|
||||
// @phpstan-ignore-next-line
|
||||
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the given test suite is a valid Pest suite.
|
||||
*
|
||||
* @param TestSuite<Test> $suite
|
||||
*/
|
||||
private static function isPestTestSuite(TestSuite $suite): bool
|
||||
{
|
||||
return strncmp($suite->getName(), 'P\\', strlen('P\\')) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test suite is made up of multiple smaller test suites.
|
||||
*
|
||||
* @param TestSuite<Test> $suite
|
||||
*/
|
||||
private static function isCompoundTestSuite(TestSuite $suite): bool
|
||||
{
|
||||
return file_exists($suite->getName()) || !method_exists($suite->getName(), '__getFileName');
|
||||
}
|
||||
|
||||
public static function isPestTest(Test $test): bool
|
||||
{
|
||||
/** @var array<string, string> $uses */
|
||||
$uses = class_uses($test);
|
||||
|
||||
return in_array(Testable::class, $uses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
$this->printEvent(self::TEST_FINISHED, [
|
||||
self::NAME => $test->getName(),
|
||||
self::DURATION => self::toMilliseconds($time),
|
||||
]);
|
||||
|
||||
if (!$this->lastTestFailed) {
|
||||
$this->writeSuccess($test->getName());
|
||||
}
|
||||
|
||||
$this->numAssertions += $test instanceof TestCase ? $test->getNumAssertions() : 1;
|
||||
$this->lastTestFailed = false;
|
||||
}
|
||||
|
||||
private static function toMilliseconds(float $time): int
|
||||
{
|
||||
return (int) round($time * 1000);
|
||||
}
|
||||
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeError($test->getName());
|
||||
$this->phpunitTeamCity->addError($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->markAsFailure($e);
|
||||
$this->writeError($test->getName());
|
||||
$this->phpunitTeamCity->addFailure($test, $e, $time);
|
||||
}
|
||||
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
$this->markAsFailure($e);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addWarning($test, $e, $time);
|
||||
}
|
||||
|
||||
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addIncompleteTest($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addRiskyTest($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time);
|
||||
}
|
||||
|
||||
private function markAsFailure(Throwable $t): void
|
||||
{
|
||||
$this->lastTestFailed = true;
|
||||
ExceptionTrace::removePestReferences($t);
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,26 @@ final class OppositeExpectation
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value array not has the provided $keys.
|
||||
*
|
||||
* @param array<int, int|string> $keys
|
||||
*/
|
||||
public function toHaveKeys(array $keys): Expectation
|
||||
{
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$this->original->toHaveKey($key);
|
||||
} catch (ExpectationFailedException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->throwExpectationFailedException('toHaveKey', [$key]);
|
||||
}
|
||||
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic method calls into the original expectation.
|
||||
*
|
||||
@ -42,7 +62,7 @@ final class OppositeExpectation
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$this->throwExpectationFailedExpection($name, $arguments);
|
||||
$this->throwExpectationFailedException($name, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,16 +78,15 @@ final class OppositeExpectation
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$this->throwExpectationFailedExpection($name);
|
||||
$this->throwExpectationFailedException($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation failed exception
|
||||
* with a nice readable message.
|
||||
* Creates a new expectation failed exception with a nice readable message.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
private function throwExpectationFailedExpection(string $name, array $arguments = []): void
|
||||
private function throwExpectationFailedException(string $name, array $arguments = []): void
|
||||
{
|
||||
$exporter = new Exporter();
|
||||
|
||||
|
||||
@ -7,15 +7,15 @@ namespace Pest\PendingObjects;
|
||||
use Closure;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\HigherOrderCallables;
|
||||
use Pest\Support\NullClosure;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\ExecutionOrderDependency;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
/**
|
||||
* @method \Pest\Expectation expect(mixed $value)
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @mixin HigherOrderCallables
|
||||
*/
|
||||
final class TestCall
|
||||
{
|
||||
@ -59,11 +59,15 @@ final class TestCall
|
||||
/**
|
||||
* Asserts that the test throws the given `$exceptionClass` when called.
|
||||
*/
|
||||
public function throws(string $exceptionClass, string $exceptionMessage = null): TestCall
|
||||
public function throws(string $exception, string $exceptionMessage = null): TestCall
|
||||
{
|
||||
if (class_exists($exception)) {
|
||||
$this->testCaseFactory
|
||||
->proxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exceptionClass]);
|
||||
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]);
|
||||
} else {
|
||||
$exceptionMessage = $exception;
|
||||
}
|
||||
|
||||
if (is_string($exceptionMessage)) {
|
||||
$this->testCaseFactory
|
||||
@ -78,11 +82,13 @@ final class TestCall
|
||||
* Runs the current test multiple times with
|
||||
* each item of the given `iterable`.
|
||||
*
|
||||
* @param \Closure|iterable<int, mixed>|string $data
|
||||
* @param array<\Closure|iterable<int|string, mixed>|string> $data
|
||||
*/
|
||||
public function with($data): TestCall
|
||||
public function with(...$data): TestCall
|
||||
{
|
||||
$this->testCaseFactory->dataset = $data;
|
||||
foreach ($data as $dataset) {
|
||||
$this->testCaseFactory->datasets[] = $dataset;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -92,15 +98,9 @@ final class TestCall
|
||||
*/
|
||||
public function depends(string ...$tests): TestCall
|
||||
{
|
||||
$className = $this->testCaseFactory->getClassName();
|
||||
|
||||
$tests = array_map(function (string $test) use ($className): ExecutionOrderDependency {
|
||||
return ExecutionOrderDependency::createFromDependsAnnotation($className, $test);
|
||||
}, $tests);
|
||||
|
||||
$this->testCaseFactory
|
||||
->factoryProxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'setDependencies', [$tests]);
|
||||
->add(Backtrace::file(), Backtrace::line(), 'addDependencies', [$tests]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -148,21 +148,40 @@ final class TestCall
|
||||
? $conditionOrMessage
|
||||
: $message;
|
||||
|
||||
if ($condition() !== false) {
|
||||
/** @var callable(): bool $condition */
|
||||
$condition = $condition->bindTo(null);
|
||||
|
||||
$this->testCaseFactory
|
||||
->chains
|
||||
->add(Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
|
||||
}
|
||||
->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the property accessors to be used on the target.
|
||||
*/
|
||||
public function __get(string $name): self
|
||||
{
|
||||
return $this->addChain($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the calls to be used on the target.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __call(string $name, array $arguments): self
|
||||
{
|
||||
return $this->addChain($name, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a chain to the test case factory. Omitting the arguments will treat it as a property accessor.
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
private function addChain(string $name, array $arguments = null): self
|
||||
{
|
||||
$this->testCaseFactory
|
||||
->chains
|
||||
@ -173,7 +192,9 @@ final class TestCall
|
||||
if ($this->testCaseFactory->description !== null) {
|
||||
$this->testCaseFactory->description .= ' → ';
|
||||
}
|
||||
$this->testCaseFactory->description .= sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
|
||||
$this->testCaseFactory->description .= $arguments === null
|
||||
? $name
|
||||
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\PendingObjects;
|
||||
|
||||
use Pest\Exceptions\InvalidUsesPath;
|
||||
use Closure;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
@ -12,6 +12,20 @@ use Pest\TestSuite;
|
||||
*/
|
||||
final class UsesCall
|
||||
{
|
||||
/**
|
||||
* Contains a global before each hook closure to be executed.
|
||||
*
|
||||
* Array indices here matter. They are mapped as follows:
|
||||
*
|
||||
* - `0` => `beforeAll`
|
||||
* - `1` => `beforeEach`
|
||||
* - `2` => `afterEach`
|
||||
* - `3` => `afterAll`
|
||||
*
|
||||
* @var array<int, Closure>
|
||||
*/
|
||||
private $hooks = [];
|
||||
|
||||
/**
|
||||
* Holds the class and traits.
|
||||
*
|
||||
@ -54,7 +68,7 @@ final class UsesCall
|
||||
|
||||
/**
|
||||
* The directories or file where the
|
||||
* class or trais should be used.
|
||||
* class or traits should be used.
|
||||
*/
|
||||
public function in(string ...$targets): void
|
||||
{
|
||||
@ -77,14 +91,13 @@ final class UsesCall
|
||||
]);
|
||||
}, $targets);
|
||||
|
||||
$this->targets = array_map(function ($target): string {
|
||||
$isValid = is_dir($target) || file_exists($target);
|
||||
if (!$isValid) {
|
||||
throw new InvalidUsesPath($target);
|
||||
$this->targets = array_reduce($targets, function (array $accumulator, string $target): array {
|
||||
if (is_dir($target) || file_exists($target)) {
|
||||
$accumulator[] = (string) realpath($target);
|
||||
}
|
||||
|
||||
return (string) realpath($target);
|
||||
}, $targets);
|
||||
return $accumulator;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,11 +110,56 @@ final class UsesCall
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global beforeAll test hook.
|
||||
*/
|
||||
public function beforeAll(Closure $hook): UsesCall
|
||||
{
|
||||
$this->hooks[0] = $hook;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global beforeEach test hook.
|
||||
*/
|
||||
public function beforeEach(Closure $hook): UsesCall
|
||||
{
|
||||
$this->hooks[1] = $hook;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global afterEach test hook.
|
||||
*/
|
||||
public function afterEach(Closure $hook): UsesCall
|
||||
{
|
||||
$this->hooks[2] = $hook;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global afterAll test hook.
|
||||
*/
|
||||
public function afterAll(Closure $hook): UsesCall
|
||||
{
|
||||
$this->hooks[3] = $hook;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the creation of uses.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
TestSuite::getInstance()->tests->use($this->classAndTraits, $this->groups, $this->targets);
|
||||
TestSuite::getInstance()->tests->use(
|
||||
$this->classAndTraits,
|
||||
$this->groups,
|
||||
$this->targets,
|
||||
$this->hooks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,5 +6,10 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '0.3.1';
|
||||
return '1.16.0';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
{
|
||||
return TestSuite::getInstance()->testPath . '/' . $file;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ final class Plugin
|
||||
public static function uses(string ...$traits): void
|
||||
{
|
||||
self::$callables[] = function () use ($traits): void {
|
||||
uses(...$traits)->in(TestSuite::getInstance()->rootPath . DIRECTORY_SEPARATOR . 'tests');
|
||||
uses(...$traits)->in(TestSuite::getInstance()->rootPath . DIRECTORY_SEPARATOR . testDirectory());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
118
src/Plugins/Coverage.php
Normal file
118
src/Plugins/Coverage.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins;
|
||||
|
||||
use Pest\Contracts\Plugins\AddsOutput;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Support\Str;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Coverage implements AddsOutput, HandlesArguments
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const COVERAGE_OPTION = 'coverage';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const MIN_OPTION = 'min';
|
||||
|
||||
/**
|
||||
* Whether should show the coverage or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $coverage = false;
|
||||
|
||||
/**
|
||||
* The minimum coverage.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
public $coverageMin = 0.0;
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function handleArguments(array $originals): array
|
||||
{
|
||||
$arguments = array_merge([''], array_values(array_filter($originals, function ($original): bool {
|
||||
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
|
||||
if ($original === sprintf('--%s', $option) || Str::startsWith($original, sprintf('--%s=', $option))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})));
|
||||
|
||||
$originals = array_flip($originals);
|
||||
foreach ($arguments as $argument) {
|
||||
unset($originals[$argument]);
|
||||
}
|
||||
$originals = array_flip($originals);
|
||||
|
||||
$inputs = [];
|
||||
$inputs[] = new InputOption(self::COVERAGE_OPTION, null, InputOption::VALUE_NONE);
|
||||
$inputs[] = new InputOption(self::MIN_OPTION, null, InputOption::VALUE_REQUIRED);
|
||||
|
||||
$input = new ArgvInput($arguments, new InputDefinition($inputs));
|
||||
if ((bool) $input->getOption(self::COVERAGE_OPTION)) {
|
||||
$this->coverage = true;
|
||||
$originals[] = '--coverage-php';
|
||||
$originals[] = \Pest\Support\Coverage::getPath();
|
||||
}
|
||||
|
||||
if ($input->getOption(self::MIN_OPTION) !== null) {
|
||||
$this->coverageMin = (float) $input->getOption(self::MIN_OPTION);
|
||||
}
|
||||
|
||||
return $originals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to add custom output after the test suite was executed.
|
||||
*/
|
||||
public function addOutput(int $result): int
|
||||
{
|
||||
if ($result === 0 && $this->coverage) {
|
||||
if (!\Pest\Support\Coverage::isAvailable()) {
|
||||
$this->output->writeln(
|
||||
"\n <fg=white;bg=red;options=bold> ERROR </> No code coverage driver is available.</>",
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$coverage = \Pest\Support\Coverage::report($this->output);
|
||||
|
||||
$result = (int) ($coverage < $this->coverageMin);
|
||||
|
||||
if ($result === 1) {
|
||||
$this->output->writeln(sprintf(
|
||||
"\n <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> %s %%</>. Minimum:<fg=white;options=bold> %s %%</>.",
|
||||
number_format($coverage, 1),
|
||||
number_format($this->coverageMin, 1)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
128
src/Plugins/Init.php
Normal file
128
src/Plugins/Init.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins;
|
||||
|
||||
use Pest\Console\Thanks;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\TestSuite;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Init implements HandlesArguments
|
||||
{
|
||||
/**
|
||||
* The option the triggers the init job.
|
||||
*/
|
||||
private const INIT_OPTION = '--init';
|
||||
|
||||
/**
|
||||
* The files that will be created.
|
||||
*/
|
||||
private const STUBS = [
|
||||
'phpunit.xml' => 'phpunit.xml',
|
||||
'Pest.php' => 'tests/Pest.php',
|
||||
'ExampleTest.php' => 'tests/ExampleTest.php',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* @var TestSuite
|
||||
*/
|
||||
private $testSuite;
|
||||
|
||||
/**
|
||||
* Creates a new Plugin instance.
|
||||
*/
|
||||
public function __construct(TestSuite $testSuite, OutputInterface $output)
|
||||
{
|
||||
$this->testSuite = $testSuite;
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function handleArguments(array $arguments): array
|
||||
{
|
||||
if (!array_key_exists(1, $arguments) || $arguments[1] !== self::INIT_OPTION) {
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
unset($arguments[1]);
|
||||
|
||||
$this->init();
|
||||
|
||||
return array_values($arguments);
|
||||
}
|
||||
|
||||
private function init(): void
|
||||
{
|
||||
$testsBaseDir = "{$this->testSuite->rootPath}/tests";
|
||||
|
||||
if (!is_dir($testsBaseDir)) {
|
||||
if (!mkdir($testsBaseDir) && !is_dir($testsBaseDir)) {
|
||||
$this->output->writeln(sprintf(
|
||||
"\n <fg=white;bg=red;options=bold> ERROR </> Directory `%s` was not created.</>",
|
||||
$testsBaseDir
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln(
|
||||
' <fg=black;bg=green;options=bold> DONE </> Created `tests` directory.</>',
|
||||
);
|
||||
}
|
||||
|
||||
foreach (self::STUBS as $from => $to) {
|
||||
$fromPath = __DIR__ . "/../../stubs/init/{$from}";
|
||||
$toPath = "{$this->testSuite->rootPath}/{$to}";
|
||||
|
||||
if (file_exists($toPath)) {
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=black;bg=yellow;options=bold> INFO </> File `%s` already exists, skipped.</>',
|
||||
$to
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($from === 'phpunit.xml' && file_exists($toPath . '.dist')) {
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=black;bg=yellow;options=bold> INFO </> File `%s` already exists, skipped.</>',
|
||||
$to . '.dist'
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!copy($fromPath, $toPath)) {
|
||||
$this->output->writeln(sprintf(
|
||||
'<fg=black;bg=red>[WARNING] Failed to copy stub `%s` to `%s`</>',
|
||||
$from,
|
||||
$toPath
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->output->writeln(sprintf(
|
||||
' <fg=black;bg=green;options=bold> DONE </> Created `%s` file.</>',
|
||||
$to
|
||||
));
|
||||
}
|
||||
|
||||
$this->output->writeln(
|
||||
"\n <fg=black;bg=green;options=bold> DONE </> Pest initialised.</>\n",
|
||||
);
|
||||
|
||||
(new Thanks($this->output))();
|
||||
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,6 @@ final class AfterEachRepository
|
||||
|
||||
return ChainableClosure::from(function (): void {
|
||||
if (class_exists(Mockery::class)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
if ($container = Mockery::getContainer()) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->addToAssertionCount($container->mockery_getExpectationCount());
|
||||
|
||||
@ -4,11 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Repositories;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\DatasetMissing;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Exceptions\TestAlreadyExist;
|
||||
use Pest\Exceptions\TestCaseAlreadyInUse;
|
||||
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\Str;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@ -18,13 +21,18 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
final class TestRepository
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const SEPARATOR = '>>>';
|
||||
|
||||
/**
|
||||
* @var array<string, TestCaseFactory>
|
||||
*/
|
||||
private $state = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<int, array<int, string>>>
|
||||
* @var array<string, array<int, array<int, string|Closure>>>
|
||||
*/
|
||||
private $uses = [];
|
||||
|
||||
@ -36,6 +44,20 @@ final class TestRepository
|
||||
return count($this->state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename of each test that should be executed in the suite.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getFilenames(): array
|
||||
{
|
||||
$testsWithOnly = $this->testsUsingOnly();
|
||||
|
||||
return array_values(array_map(function (TestCaseFactory $factory): string {
|
||||
return $factory->filename;
|
||||
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given callable foreach test case.
|
||||
*/
|
||||
@ -46,12 +68,13 @@ final class TestRepository
|
||||
};
|
||||
|
||||
foreach ($this->uses as $path => $uses) {
|
||||
[$classOrTraits, $groups] = $uses;
|
||||
$setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith): void {
|
||||
[$filename] = explode('@', $key);
|
||||
[$classOrTraits, $groups, $hooks] = $uses;
|
||||
|
||||
$setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith, $hooks): void {
|
||||
[$filename] = explode(self::SEPARATOR, $key);
|
||||
|
||||
if ((!is_dir($path) && $filename === $path) || (is_dir($path) && $startsWith($filename, $path))) {
|
||||
foreach ($classOrTraits as $class) {
|
||||
foreach ($classOrTraits as $class) { /** @var string $class */
|
||||
if (class_exists($class)) {
|
||||
if ($testCase->class !== TestCase::class) {
|
||||
throw new TestCaseAlreadyInUse($testCase->class, $class, $filename);
|
||||
@ -62,10 +85,12 @@ final class TestRepository
|
||||
}
|
||||
}
|
||||
|
||||
$testCase
|
||||
->factoryProxies
|
||||
// Consider set the real line here.
|
||||
->add($filename, 0, 'addGroups', [$groups]);
|
||||
// IDEA: Consider set the real lines on these.
|
||||
$testCase->factoryProxies->add($filename, 0, 'addGroups', [$groups]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addBeforeAll', [$hooks[0] ?? null]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addBeforeEach', [$hooks[1] ?? null]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addAfterEach', [$hooks[2] ?? null]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addAfterAll', [$hooks[3] ?? null]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -74,14 +99,12 @@ final class TestRepository
|
||||
}
|
||||
}
|
||||
|
||||
$onlyState = array_filter($this->state, function ($testFactory): bool {
|
||||
return $testFactory->only;
|
||||
});
|
||||
$onlyState = $this->testsUsingOnly();
|
||||
|
||||
$state = count($onlyState) > 0 ? $onlyState : $this->state;
|
||||
|
||||
foreach ($state as $testFactory) {
|
||||
/* @var TestCaseFactory $testFactory */
|
||||
/** @var TestCaseFactory $testFactory */
|
||||
$tests = $testFactory->build($testSuite);
|
||||
foreach ($tests as $test) {
|
||||
$each($test);
|
||||
@ -89,14 +112,27 @@ final class TestRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all tests that have called the only method.
|
||||
*
|
||||
* @return array<TestCaseFactory>
|
||||
*/
|
||||
private function testsUsingOnly(): array
|
||||
{
|
||||
return array_filter($this->state, function ($testFactory): bool {
|
||||
return $testFactory->only;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the given `$testCaseClass` on the given `$paths`.
|
||||
*
|
||||
* @param array<int, string> $classOrTraits
|
||||
* @param array<int, string> $groups
|
||||
* @param array<int, string> $paths
|
||||
* @param array<int, Closure> $hooks
|
||||
*/
|
||||
public function use(array $classOrTraits, array $groups, array $paths): void
|
||||
public function use(array $classOrTraits, array $groups, array $paths, array $hooks): void
|
||||
{
|
||||
foreach ($classOrTraits as $classOrTrait) {
|
||||
if (!class_exists($classOrTrait) && !trait_exists($classOrTrait)) {
|
||||
@ -109,9 +145,10 @@ final class TestRepository
|
||||
$this->uses[$path] = [
|
||||
array_merge($this->uses[$path][0], $classOrTraits),
|
||||
array_merge($this->uses[$path][1], $groups),
|
||||
$this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices
|
||||
];
|
||||
} else {
|
||||
$this->uses[$path] = [$classOrTraits, $groups];
|
||||
$this->uses[$path] = [$classOrTraits, $groups, $hooks];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,10 +162,18 @@ final class TestRepository
|
||||
throw ShouldNotHappen::fromMessage('Trying to create a test without description.');
|
||||
}
|
||||
|
||||
if (array_key_exists(sprintf('%s@%s', $test->filename, $test->description), $this->state)) {
|
||||
if (array_key_exists(sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description), $this->state)) {
|
||||
throw new TestAlreadyExist($test->filename, $test->description);
|
||||
}
|
||||
|
||||
$this->state[sprintf('%s@%s', $test->filename, $test->description)] = $test;
|
||||
if (!$test->receivesArguments()) {
|
||||
$arguments = Reflection::getFunctionArguments($test->test);
|
||||
|
||||
if (count($arguments) > 0) {
|
||||
throw new DatasetMissing($test->filename, $test->description, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
$this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test;
|
||||
}
|
||||
}
|
||||
|
||||
68
src/Support/Arr.php
Normal file
68
src/Support/Arr.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
/**
|
||||
* Credits: most of this class methods and implementations
|
||||
* belongs to the Arr helper of laravel/framework project
|
||||
* (https://github.com/laravel/framework).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Arr
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $array
|
||||
* @param string|int $key
|
||||
*/
|
||||
public static function has(array $array, $key): bool
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
if (array_key_exists($key, $array)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (is_array($array) && array_key_exists($segment, $array)) {
|
||||
$array = $array[$segment];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $array
|
||||
* @param string|int $key
|
||||
* @param null $default
|
||||
*
|
||||
* @return array|mixed|null
|
||||
*/
|
||||
public static function get(array $array, $key, $default = null)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
if (array_key_exists($key, $array)) {
|
||||
return $array[$key];
|
||||
}
|
||||
|
||||
if (strpos($key, '.') === false) {
|
||||
return $array[$key] ?? $default;
|
||||
}
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (is_array($array) && array_key_exists($segment, $array)) {
|
||||
$array = $array[$segment];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@ -12,13 +12,28 @@ use Closure;
|
||||
final class ChainableClosure
|
||||
{
|
||||
/**
|
||||
* Calls the given `$closure` and chains the the `$next` closure.
|
||||
* Calls the given `$closure` and chains the `$next` closure.
|
||||
*/
|
||||
public static function from(Closure $closure, Closure $next): Closure
|
||||
{
|
||||
return function () use ($closure, $next): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($closure, $this, get_class($this)), func_get_args());
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($next, $this, get_class($this)), func_get_args());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the given static `$closure` and chains the `$next` closure.
|
||||
*/
|
||||
public static function fromStatic(Closure $closure, Closure $next): Closure
|
||||
{
|
||||
return static function () use ($closure, $next): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($closure, null, self::class), func_get_args());
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($next, null, self::class), func_get_args());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@ final class Container
|
||||
|
||||
if ($candidate === null) {
|
||||
$type = $param->getType();
|
||||
/* @phpstan-ignore-next-line */
|
||||
if ($type !== null && $type->isBuiltin()) {
|
||||
$candidate = $param->getName();
|
||||
} else {
|
||||
|
||||
169
src/Support/Coverage.php
Normal file
169
src/Support/Coverage.php
Normal file
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
||||
use SebastianBergmann\CodeCoverage\Node\Directory;
|
||||
use SebastianBergmann\CodeCoverage\Node\File;
|
||||
use SebastianBergmann\Environment\Runtime;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Coverage
|
||||
{
|
||||
/**
|
||||
* Returns the coverage path.
|
||||
*/
|
||||
public static function getPath(): string
|
||||
{
|
||||
return implode(DIRECTORY_SEPARATOR, [
|
||||
dirname(__DIR__, 2),
|
||||
'.temp',
|
||||
'coverage.php',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs true there is any code
|
||||
* coverage driver available.
|
||||
*/
|
||||
public static function isAvailable(): bool
|
||||
{
|
||||
return (new Runtime())->canCollectCodeCoverage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the code coverage report to the
|
||||
* console and returns the result in float.
|
||||
*/
|
||||
public static function report(OutputInterface $output): float
|
||||
{
|
||||
if (!file_exists($reportPath = self::getPath())) {
|
||||
throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath));
|
||||
}
|
||||
|
||||
/** @var CodeCoverage $codeCoverage */
|
||||
$codeCoverage = require $reportPath;
|
||||
unlink($reportPath);
|
||||
|
||||
$totalWidth = (new Terminal())->getWidth();
|
||||
|
||||
$dottedLineLength = $totalWidth <= 70 ? $totalWidth : 70;
|
||||
|
||||
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
|
||||
|
||||
$output->writeln(
|
||||
sprintf(
|
||||
' <fg=white;options=bold>Cov: </><fg=default>%s</>',
|
||||
$totalCoverage->asString()
|
||||
)
|
||||
);
|
||||
|
||||
$output->writeln('');
|
||||
|
||||
/** @var Directory<File|Directory> $report */
|
||||
$report = $codeCoverage->getReport();
|
||||
|
||||
foreach ($report->getIterator() as $file) {
|
||||
if (!$file instanceof File) {
|
||||
continue;
|
||||
}
|
||||
$dirname = dirname($file->id());
|
||||
$basename = basename($file->id(), '.php');
|
||||
|
||||
$name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
|
||||
$dirname,
|
||||
$basename,
|
||||
]);
|
||||
$rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
|
||||
$dirname,
|
||||
$basename,
|
||||
]);
|
||||
|
||||
$linesExecutedTakenSize = 0;
|
||||
|
||||
if ($file->percentageOfExecutedLines()->asString() != '0.00%') {
|
||||
$linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1;
|
||||
$name .= sprintf(' <fg=red>%s</>', $uncoveredLines);
|
||||
}
|
||||
|
||||
$percentage = $file->numberOfExecutableLines() === 0
|
||||
? '100.0'
|
||||
: number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', '');
|
||||
|
||||
$takenSize = strlen($rawName . $percentage) + 4 + $linesExecutedTakenSize; // adding 3 space and percent sign
|
||||
|
||||
$percentage = sprintf(
|
||||
'<fg=%s>%s</>',
|
||||
$percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'),
|
||||
$percentage
|
||||
);
|
||||
|
||||
$output->writeln(sprintf(
|
||||
' %s %s %s %%',
|
||||
$name,
|
||||
str_repeat('.', max($dottedLineLength - $takenSize, 1)),
|
||||
$percentage
|
||||
));
|
||||
}
|
||||
|
||||
return $totalCoverage->asFloat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of missing coverage on the following format:.
|
||||
*
|
||||
* ```
|
||||
* ['11', '20..25', '50', '60..80'];
|
||||
* ```
|
||||
*
|
||||
* @param File $file
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function getMissingCoverage($file): array
|
||||
{
|
||||
$shouldBeNewLine = true;
|
||||
|
||||
$eachLine = function (array $array, array $tests, int $line) use (&$shouldBeNewLine): array {
|
||||
if (count($tests) > 0) {
|
||||
$shouldBeNewLine = true;
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
if ($shouldBeNewLine) {
|
||||
$array[] = (string) $line;
|
||||
$shouldBeNewLine = false;
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
$lastKey = count($array) - 1;
|
||||
|
||||
if (array_key_exists($lastKey, $array) && strpos($array[$lastKey], '..') !== false) {
|
||||
[$from] = explode('..', $array[$lastKey]);
|
||||
$array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
$array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line);
|
||||
|
||||
return $array;
|
||||
};
|
||||
|
||||
$array = [];
|
||||
foreach (array_filter($file->lineCoverageData(), 'is_array') as $line => $tests) {
|
||||
$array = $eachLine($array, $tests, $line);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
use ReflectionProperty;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@ -36,4 +37,30 @@ final class ExceptionTrace
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any item from the stack trace referencing Pest so as not to
|
||||
* crowd the error log for the end user.
|
||||
*/
|
||||
public static function removePestReferences(Throwable $t): void
|
||||
{
|
||||
if (!property_exists($t, 'serializableTrace')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property = new ReflectionProperty($t, 'serializableTrace');
|
||||
$property->setAccessible(true);
|
||||
$trace = $property->getValue($t);
|
||||
|
||||
$cleanedTrace = [];
|
||||
foreach ($trace as $item) {
|
||||
if (key_exists('file', $item) && mb_strpos($item['file'], 'vendor/pestphp/pest/') > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cleanedTrace[] = $item;
|
||||
}
|
||||
|
||||
$property->setValue($t, $cleanedTrace);
|
||||
}
|
||||
}
|
||||
|
||||
33
src/Support/Extendable.php
Normal file
33
src/Support/Extendable.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
|
||||
final class Extendable
|
||||
{
|
||||
/**
|
||||
* The extendable class.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $extendableClass;
|
||||
|
||||
/**
|
||||
* Creates a new extendable instance.
|
||||
*/
|
||||
public function __construct(string $extendableClass)
|
||||
{
|
||||
$this->extendableClass = $extendableClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom extend.
|
||||
*/
|
||||
public function extend(string $name, Closure $extend): void
|
||||
{
|
||||
$this->extendableClass::extend($name, $extend);
|
||||
}
|
||||
}
|
||||
66
src/Support/HigherOrderCallables.php
Normal file
66
src/Support/HigherOrderCallables.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
use Pest\Expectation;
|
||||
use Pest\PendingObjects\TestCall;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class HigherOrderCallables
|
||||
{
|
||||
/**
|
||||
* @var object
|
||||
*/
|
||||
private $target;
|
||||
|
||||
public function __construct(object $target)
|
||||
{
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TValue
|
||||
*
|
||||
* Create a new expectation. Callable values will be executed prior to returning the new expectation.
|
||||
*
|
||||
* @param callable|TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function expect($value)
|
||||
{
|
||||
return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TValue
|
||||
*
|
||||
* Create a new expectation. Callable values will be executed prior to returning the new expectation.
|
||||
*
|
||||
* @param callable|TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function and($value)
|
||||
{
|
||||
return $this->expect($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tap into the test case to perform an action and return the test case.
|
||||
*
|
||||
* @return TestCall|TestCase|object
|
||||
*/
|
||||
public function tap(callable $callable)
|
||||
{
|
||||
Reflection::bindCallableWithData($callable);
|
||||
|
||||
return $this->target;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
use ReflectionClass;
|
||||
use Throwable;
|
||||
|
||||
@ -33,33 +34,40 @@ final class HigherOrderMessage
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* The method name.
|
||||
* The method or property name to access.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $methodName;
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The arguments.
|
||||
*
|
||||
* @var array<int, mixed>
|
||||
* @var array<int, mixed>|null
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
public $arguments;
|
||||
|
||||
/**
|
||||
* An optional condition that will determine if the message will be executed.
|
||||
*
|
||||
* @var callable(): bool|null
|
||||
*/
|
||||
public $condition = null;
|
||||
|
||||
/**
|
||||
* Creates a new higher order message.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function __construct(string $filename, int $line, string $methodName, array $arguments)
|
||||
public function __construct(string $filename, int $line, string $methodName, $arguments)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->line = $line;
|
||||
$this->methodName = $methodName;
|
||||
$this->name = $methodName;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
@ -70,24 +78,58 @@ final class HigherOrderMessage
|
||||
*/
|
||||
public function call(object $target)
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
if ($this->hasHigherOrderCallable()) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
return (new HigherOrderCallables($target))->{$this->name}(...$this->arguments);
|
||||
}
|
||||
|
||||
try {
|
||||
return Reflection::call($target, $this->methodName, $this->arguments);
|
||||
return is_array($this->arguments)
|
||||
? Reflection::call($target, $this->name, $this->arguments)
|
||||
: $target->{$this->name}; /* @phpstan-ignore-line */
|
||||
} catch (Throwable $throwable) {
|
||||
Reflection::setPropertyValue($throwable, 'file', $this->filename);
|
||||
Reflection::setPropertyValue($throwable, 'line', $this->line);
|
||||
|
||||
if ($throwable->getMessage() === self::getUndefinedMethodMessage($target, $this->methodName)) {
|
||||
if ($throwable->getMessage() === self::getUndefinedMethodMessage($target, $this->name)) {
|
||||
/** @var ReflectionClass $reflection */
|
||||
$reflection = new ReflectionClass($target);
|
||||
/* @phpstan-ignore-next-line */
|
||||
$reflection = $reflection->getParentClass() ?: $reflection;
|
||||
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->methodName));
|
||||
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $this->name));
|
||||
}
|
||||
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this message should only be called when the given condition is true.
|
||||
*
|
||||
* @param callable(): bool $condition
|
||||
*/
|
||||
public function when(callable $condition): self
|
||||
{
|
||||
$this->condition = $condition;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not there exists a higher order callable with the message name.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function hasHigherOrderCallable()
|
||||
{
|
||||
return in_array($this->name, get_class_methods(HigherOrderCallables::class), true);
|
||||
}
|
||||
|
||||
private static function getUndefinedMethodMessage(object $target, string $methodName): string
|
||||
{
|
||||
if (\PHP_MAJOR_VERSION >= 8) {
|
||||
|
||||
@ -17,11 +17,21 @@ final class HigherOrderMessageCollection
|
||||
/**
|
||||
* Adds a new higher order message to the collection.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function add(string $filename, int $line, string $methodName, array $arguments): void
|
||||
public function add(string $filename, int $line, string $name, array $arguments = null): void
|
||||
{
|
||||
$this->messages[] = new HigherOrderMessage($filename, $line, $methodName, $arguments);
|
||||
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new higher order message to the collection if the callable condition is does not return false.
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function addWhen(callable $condition, string $filename, int $line, string $name, array $arguments = null): void
|
||||
{
|
||||
$this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,7 +40,7 @@ final class HigherOrderMessageCollection
|
||||
public function chain(object $target): void
|
||||
{
|
||||
foreach ($this->messages as $message) {
|
||||
$target = $message->call($target);
|
||||
$target = $message->call($target) ?? $target;
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,4 +53,20 @@ final class HigherOrderMessageCollection
|
||||
$message->call($target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of messages with the given name.
|
||||
*
|
||||
* @param string $name A higher order message name (usually a method name)
|
||||
*/
|
||||
public function count(string $name): int
|
||||
{
|
||||
return array_reduce(
|
||||
$this->messages,
|
||||
static function (int $total, HigherOrderMessage $message) use ($name): int {
|
||||
return $total + (int) ($name === $message->name);
|
||||
},
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use Throwable;
|
||||
|
||||
@ -17,16 +18,14 @@ final class HigherOrderTapProxy
|
||||
/**
|
||||
* The target being tapped.
|
||||
*
|
||||
* @var mixed
|
||||
* @var TestCase
|
||||
*/
|
||||
public $target;
|
||||
|
||||
/**
|
||||
* Create a new tap proxy instance.
|
||||
*
|
||||
* @param mixed $target
|
||||
*/
|
||||
public function __construct($target)
|
||||
public function __construct(TestCase $target)
|
||||
{
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ use ReflectionException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use ReflectionProperty;
|
||||
use ReflectionUnionType;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -42,15 +42,40 @@ final class Reflection
|
||||
}
|
||||
|
||||
if (is_callable($method)) {
|
||||
return Closure::fromCallable($method)->bindTo(
|
||||
TestSuite::getInstance()->test
|
||||
)(...$args);
|
||||
return static::bindCallable($method, $args);
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a callable to the TestCase and return the result.
|
||||
*
|
||||
* @param array<int, mixed> $args
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function bindCallable(callable $callable, array $args = [])
|
||||
{
|
||||
return Closure::fromCallable($callable)->bindTo(TestSuite::getInstance()->test)(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a callable to the TestCase and return the result,
|
||||
* passing in the current dataset values as arguments.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function bindCallableWithData(callable $callable)
|
||||
{
|
||||
$test = TestSuite::getInstance()->test;
|
||||
|
||||
return $test === null
|
||||
? static::bindCallable($callable)
|
||||
: Closure::fromCallable($callable)->bindTo($test)(...$test->getProvidedData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers the file name from the given closure.
|
||||
*/
|
||||
@ -85,10 +110,6 @@ final class Reflection
|
||||
}
|
||||
}
|
||||
|
||||
if ($reflectionProperty === null) {
|
||||
throw ShouldNotHappen::fromMessage('Reflection property not found.');
|
||||
}
|
||||
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
return $reflectionProperty->getValue($object);
|
||||
@ -119,10 +140,6 @@ final class Reflection
|
||||
}
|
||||
}
|
||||
|
||||
if ($reflectionProperty === null) {
|
||||
throw ShouldNotHappen::fromMessage('Reflection property not found.');
|
||||
}
|
||||
|
||||
$reflectionProperty->setAccessible(true);
|
||||
$reflectionProperty->setValue($object, $value);
|
||||
}
|
||||
@ -154,4 +171,37 @@ final class Reflection
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive a map of function argument names to their types.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getFunctionArguments(Closure $function): array
|
||||
{
|
||||
$parameters = (new ReflectionFunction($function))->getParameters();
|
||||
$arguments = [];
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
/** @var ReflectionNamedType|ReflectionUnionType|null $types */
|
||||
$types = ($parameter->hasType()) ? $parameter->getType() : null;
|
||||
|
||||
if (is_null($types)) {
|
||||
$arguments[$parameter->getName()] = 'mixed';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$arguments[$parameter->getName()] = implode('|', array_map(
|
||||
static function (ReflectionNamedType $type): string {
|
||||
return $type->getName();
|
||||
},
|
||||
($types instanceof ReflectionNamedType)
|
||||
? [$types] // NOTE: normalize as list of to handle unions
|
||||
: $types->getTypes(),
|
||||
));
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,25 @@ namespace Pest\Support;
|
||||
*/
|
||||
final class Str
|
||||
{
|
||||
/**
|
||||
* Pool of alpha-numeric characters for generating (unsafe) random strings
|
||||
* from.
|
||||
*/
|
||||
private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
/**
|
||||
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
|
||||
* string value.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static function random(int $length = 16): string
|
||||
{
|
||||
return substr(str_shuffle(str_repeat(self::POOL, 5)), 0, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given `$target` starts with the given `$search`.
|
||||
*/
|
||||
|
||||
235
src/TeamCity.php
235
src/TeamCity.php
@ -1,235 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use function getmypid;
|
||||
use Pest\Concerns\TestCase;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestResult;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
use function round;
|
||||
use function str_replace;
|
||||
use Throwable;
|
||||
|
||||
final class TeamCity extends DefaultResultPrinter
|
||||
{
|
||||
private const PROTOCOL = 'pest_qn://';
|
||||
private const NAME = 'name';
|
||||
private const LOCATION_HINT = 'locationHint';
|
||||
private const DURATION = 'duration';
|
||||
private const TEST_SUITE_STARTED = 'testSuiteStarted';
|
||||
private const TEST_SUITE_FINISHED = 'testSuiteFinished';
|
||||
private const TEST_FAILED = 'testFailed';
|
||||
|
||||
/** @var int */
|
||||
private $flowId;
|
||||
|
||||
/** @var bool */
|
||||
private $isSummaryTestCountPrinted = false;
|
||||
|
||||
/** @var \PHPUnit\Util\Log\TeamCity */
|
||||
private $phpunitTeamCity;
|
||||
|
||||
public function __construct(bool $verbose, string $colors)
|
||||
{
|
||||
parent::__construct(null, $verbose, $colors, false, 80, false);
|
||||
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity(
|
||||
null,
|
||||
$verbose,
|
||||
$colors,
|
||||
false,
|
||||
80,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public function printResult(TestResult $result): void
|
||||
{
|
||||
$this->printHeader($result);
|
||||
$this->printFooter($result);
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$this->flowId = getmypid();
|
||||
|
||||
if (!$this->isSummaryTestCountPrinted) {
|
||||
$this->printEvent(
|
||||
'testCount',
|
||||
['count' => $suite->count()]
|
||||
);
|
||||
$this->isSummaryTestCountPrinted = true;
|
||||
}
|
||||
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) {
|
||||
$this->printEvent(
|
||||
self::TEST_SUITE_STARTED, [
|
||||
self::NAME => $suiteName,
|
||||
self::LOCATION_HINT => self::PROTOCOL . $suiteName,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$fileName = $suiteName::__getFileName();
|
||||
|
||||
$this->printEvent(
|
||||
self::TEST_SUITE_STARTED, [
|
||||
self::NAME => substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . $fileName,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) {
|
||||
$this->printEvent(
|
||||
self::TEST_SUITE_FINISHED, [
|
||||
self::NAME => $suiteName,
|
||||
self::LOCATION_HINT => self::PROTOCOL . $suiteName,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent(
|
||||
self::TEST_SUITE_FINISHED, [
|
||||
self::NAME => substr($suiteName, 2),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|TestCase $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->startTest($test);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent('testStarted', [
|
||||
self::NAME => $test->getName(),
|
||||
/* @phpstan-ignore-next-line */
|
||||
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|TestCase $test
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->endTest($test, $time);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent('testFinished', [
|
||||
self::NAME => $test->getName(),
|
||||
self::DURATION => self::toMilliseconds($time),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|TestCase $test
|
||||
*/
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->addError($test, $t, $time);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent(
|
||||
self::TEST_FAILED, [
|
||||
self::NAME => $test->getName(),
|
||||
'message' => $t->getMessage(),
|
||||
'details' => $t->getTraceAsString(),
|
||||
self::DURATION => self::toMilliseconds($time),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*
|
||||
* @param Test|TestCase $test
|
||||
*/
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->addWarning($test, $e, $time);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent(
|
||||
self::TEST_FAILED, [
|
||||
self::NAME => $test->getName(),
|
||||
'message' => $e->getMessage(),
|
||||
'details' => $e->getTraceAsString(),
|
||||
self::DURATION => self::toMilliseconds($time),
|
||||
]);
|
||||
}
|
||||
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->phpunitTeamCity->addFailure($test, $e, $time);
|
||||
}
|
||||
|
||||
protected function writeProgress(string $progress): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|int> $params
|
||||
*/
|
||||
private function printEvent(string $eventName, array $params = []): void
|
||||
{
|
||||
$this->write("\n##teamcity[{$eventName}");
|
||||
|
||||
if ($this->flowId !== 0) {
|
||||
$params['flowId'] = $this->flowId;
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$escapedValue = self::escapeValue((string) $value);
|
||||
$this->write(" {$key}='{$escapedValue}'");
|
||||
}
|
||||
|
||||
$this->write("]\n");
|
||||
}
|
||||
|
||||
private static function escapeValue(string $text): string
|
||||
{
|
||||
return str_replace(
|
||||
['|', "'", "\n", "\r", ']', '['],
|
||||
['||', "|'", '|n', '|r', '|]', '|['],
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
private static function toMilliseconds(float $time): int
|
||||
{
|
||||
return (int) round($time * 1000);
|
||||
}
|
||||
|
||||
private static function isPestTest(Test $test): bool
|
||||
{
|
||||
return in_array(TestCase::class, class_uses($test), true);
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,13 @@ final class TestSuite
|
||||
*/
|
||||
public $rootPath;
|
||||
|
||||
/**
|
||||
* Holds the test path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $testPath;
|
||||
|
||||
/**
|
||||
* Holds an instance of the test suite.
|
||||
*
|
||||
@ -76,7 +83,7 @@ final class TestSuite
|
||||
/**
|
||||
* Creates a new instance of the test suite.
|
||||
*/
|
||||
public function __construct(string $rootPath)
|
||||
public function __construct(string $rootPath, string $testPath)
|
||||
{
|
||||
$this->beforeAll = new BeforeAllRepository();
|
||||
$this->beforeEach = new BeforeEachRepository();
|
||||
@ -85,15 +92,16 @@ final class TestSuite
|
||||
$this->afterAll = new AfterAllRepository();
|
||||
|
||||
$this->rootPath = (string) realpath($rootPath);
|
||||
$this->testPath = $testPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current instance of the test suite.
|
||||
*/
|
||||
public static function getInstance(string $rootPath = null): TestSuite
|
||||
public static function getInstance(string $rootPath = null, string $testPath = null): TestSuite
|
||||
{
|
||||
if (is_string($rootPath)) {
|
||||
self::$instance = new TestSuite($rootPath);
|
||||
if (is_string($rootPath) && is_string($testPath)) {
|
||||
self::$instance = new TestSuite($rootPath, $testPath);
|
||||
|
||||
foreach (Plugin::$callables as $callable) {
|
||||
$callable();
|
||||
|
||||
119
src/globals.php
119
src/globals.php
@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Pest\Datasets;
|
||||
use Pest\Expectation;
|
||||
use Pest\PendingObjects\AfterEachCall;
|
||||
use Pest\PendingObjects\BeforeEachCall;
|
||||
use Pest\PendingObjects\TestCall;
|
||||
use Pest\PendingObjects\UsesCall;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\HigherOrderTapProxy;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Runs the given closure before all tests in the current file.
|
||||
*/
|
||||
function beforeAll(Closure $closure): void
|
||||
{
|
||||
TestSuite::getInstance()->beforeAll->set($closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given closure before each test in the current file.
|
||||
*
|
||||
* @return BeforeEachCall|TestCase|mixed
|
||||
*/
|
||||
function beforeEach(Closure $closure = null): BeforeEachCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new BeforeEachCall(TestSuite::getInstance(), $filename, $closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given dataset.
|
||||
*
|
||||
* @param Closure|iterable $dataset
|
||||
*/
|
||||
function dataset(string $name, $dataset): void
|
||||
{
|
||||
Datasets::set($name, $dataset);
|
||||
}
|
||||
|
||||
/**
|
||||
* The uses function binds the given
|
||||
* arguments to test closures.
|
||||
*/
|
||||
function uses(string ...$classAndTraits): UsesCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new UsesCall($filename, $classAndTraits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given closure as a test. The first argument
|
||||
* is the test description; the second argument is
|
||||
* a closure that contains the test expectations.
|
||||
*
|
||||
* @return TestCall|TestCase|mixed
|
||||
*/
|
||||
function test(string $description = null, Closure $closure = null)
|
||||
{
|
||||
if ($description === null && TestSuite::getInstance()->test) {
|
||||
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
|
||||
}
|
||||
|
||||
$filename = Backtrace::testFile();
|
||||
|
||||
return new TestCall(TestSuite::getInstance(), $filename, $description, $closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given closure as a test. The first argument
|
||||
* is the test description; the second argument is
|
||||
* a closure that contains the test expectations.
|
||||
*
|
||||
* @return TestCall|TestCase|mixed
|
||||
*/
|
||||
function it(string $description, Closure $closure = null): TestCall
|
||||
{
|
||||
$description = sprintf('it %s', $description);
|
||||
|
||||
return test($description, $closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given closure after each test in the current file.
|
||||
*
|
||||
* @return AfterEachCall|TestCase|mixed
|
||||
*/
|
||||
function afterEach(Closure $closure = null): AfterEachCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new AfterEachCall(TestSuite::getInstance(), $filename, $closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given closure after all tests in the current file.
|
||||
*/
|
||||
function afterAll(Closure $closure = null): void
|
||||
{
|
||||
TestSuite::getInstance()->afterAll->set($closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value the Value
|
||||
*
|
||||
* @return Expectation
|
||||
*/
|
||||
function expect($value)
|
||||
{
|
||||
return test()->expect($value);
|
||||
}
|
||||
10
stubs/Browser.php
Normal file
10
stubs/Browser.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
it('has {name} page', function () {
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser->visit('/{name}')
|
||||
->assertSee('{name}');
|
||||
});
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
/**
|
||||
* A basic assert example.
|
||||
*/
|
||||
function assertExample(): void
|
||||
{
|
||||
test()->assertTrue(true);
|
||||
}
|
||||
@ -1,3 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "uses()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
uses(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</include>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
/**
|
||||
* A basic assert example.
|
||||
*/
|
||||
function assertExample(): void
|
||||
{
|
||||
test()->assertTrue(true);
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
<?php
|
||||
|
||||
uses(TestCase::class)->in(__DIR__);
|
||||
@ -1,5 +1,5 @@
|
||||
<?php
|
||||
|
||||
test('{name}', function () {
|
||||
assertTrue(true);
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
5
stubs/init/ExampleTest.php
Normal file
5
stubs/init/ExampleTest.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
test('example', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
45
stubs/init/Pest.php
Normal file
45
stubs/init/Pest.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Test Case
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
||||
| need to change it using the "uses()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
|
||||
// uses(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
||||
|
|
||||
*/
|
||||
|
||||
expect()->extend('toBeOne', function () {
|
||||
return $this->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Functions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
|
||||
| project that you don't want to repeat in every file. Here you can also expose helpers as
|
||||
| global functions to help you to reduce the number of lines of code in your test files.
|
||||
|
|
||||
*/
|
||||
|
||||
function something()
|
||||
{
|
||||
// ..
|
||||
}
|
||||
@ -5,13 +5,14 @@
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Application Test Suite">
|
||||
<testsuite name="Test Suite">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
5
tests/.snapshots/help-command.txt
Normal file
5
tests/.snapshots/help-command.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Pest Options:
|
||||
--init Initialise a standard Pest configuration
|
||||
--coverage Enable coverage and output to standard output
|
||||
--min=<N> Set the minimum required coverage percentage (<N>), and fail if not met
|
||||
--group=<name> Only runs tests from the specified group(s)
|
||||
@ -2,191 +2,6 @@
|
||||
PASS Tests\CustomTestCase\ExecutedTest
|
||||
✓ that gets executed
|
||||
|
||||
PASS Tests\Expect\not
|
||||
✓ not property calls
|
||||
|
||||
PASS Tests\Expect\toBe
|
||||
✓ expect true → toBeTrue → and false → toBeFalse
|
||||
✓ strict comparisons
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeArray
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeBool
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeCallable
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeDirectory
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeEmpty
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeFalse
|
||||
✓ strict comparisons
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeFile
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeFloat
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeGreatherThan
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeGreatherThanOrEqual
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeInfinite
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeInstanceOf
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeInt
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeIterable
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeLessThan
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeLessThanOrEqual
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeNAN
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeNull
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeNumeric
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeObject
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeReadableDirectory
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeReadableFile
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeResource
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeScalar
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeString
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeTrue
|
||||
✓ strict comparisons
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeWritableDirectory
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toBeWritableFile
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toContain
|
||||
✓ passes strings
|
||||
✓ passes arrays
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toEqual
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toEqualCanonicalizing
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toEqualWithDelta
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toHaveCount
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toHaveKey
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Expect\toHaveProperty
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\AfterAll
|
||||
✓ deletes file after all
|
||||
|
||||
@ -202,6 +17,12 @@
|
||||
✓ it gets executed before each test
|
||||
✓ it gets executed before each test once again
|
||||
|
||||
PASS Tests\Features\Coverage
|
||||
✓ it has plugin
|
||||
✓ it adds coverage if --coverage exist
|
||||
✓ it adds coverage if --min exist
|
||||
✓ it generates coverage based on file input
|
||||
|
||||
PASS Tests\Features\Datasets
|
||||
✓ it throws exception if dataset does not exist
|
||||
✓ it throws exception if dataset already exist
|
||||
@ -225,6 +46,9 @@
|
||||
✓ eager wrapped registered datasets with (1)
|
||||
✓ eager wrapped registered datasets with (2)
|
||||
✓ eager registered wrapped datasets did the job right
|
||||
✓ named datasets with data set "one"
|
||||
✓ named datasets with data set "two"
|
||||
✓ named datasets did the job right
|
||||
✓ lazy named datasets with (Bar Object (...))
|
||||
✓ it creates unique test case names with ('Name 1', Pest\Plugin Object (), true) #1
|
||||
✓ it creates unique test case names with ('Name 1', Pest\Plugin Object (), true) #2
|
||||
@ -233,11 +57,397 @@
|
||||
✓ it creates unique test case names with ('Name 2', Pest\Plugin Object (), true)
|
||||
✓ it creates unique test case names with ('Name 1', Pest\Plugin Object (), true) #3
|
||||
✓ it creates unique test case names - count
|
||||
✓ lazy multiple datasets with (1) / (3)
|
||||
✓ lazy multiple datasets with (1) / (4)
|
||||
✓ lazy multiple datasets with (2) / (3)
|
||||
✓ lazy multiple datasets with (2) / (4)
|
||||
✓ lazy multiple datasets did the job right
|
||||
✓ eager multiple datasets with (1) / (3)
|
||||
✓ eager multiple datasets with (1) / (4)
|
||||
✓ eager multiple datasets with (2) / (3)
|
||||
✓ eager multiple datasets with (2) / (4)
|
||||
✓ eager multiple datasets did the job right
|
||||
✓ lazy registered multiple datasets with (1) / (1)
|
||||
✓ lazy registered multiple datasets with (1) / (2)
|
||||
✓ lazy registered multiple datasets with (2) / (1)
|
||||
✓ lazy registered multiple datasets with (2) / (2)
|
||||
✓ lazy registered multiple datasets did the job right
|
||||
✓ eager registered multiple datasets with (1) / (1)
|
||||
✓ eager registered multiple datasets with (1) / (2)
|
||||
✓ eager registered multiple datasets with (2) / (1)
|
||||
✓ eager registered multiple datasets with (2) / (2)
|
||||
✓ eager registered multiple datasets did the job right
|
||||
✓ eager wrapped registered multiple datasets with (1) / (1)
|
||||
✓ eager wrapped registered multiple datasets with (1) / (2)
|
||||
✓ eager wrapped registered multiple datasets with (2) / (1)
|
||||
✓ eager wrapped registered multiple datasets with (2) / (2)
|
||||
✓ eager wrapped registered multiple datasets did the job right
|
||||
✓ named multiple datasets with data set "one" / data set "three"
|
||||
✓ named multiple datasets with data set "one" / data set "four"
|
||||
✓ named multiple datasets with data set "two" / data set "three"
|
||||
✓ named multiple datasets with data set "two" / data set "four"
|
||||
✓ named multiple datasets did the job right
|
||||
✓ more than two datasets with (1) / (3) / (5)
|
||||
✓ more than two datasets with (1) / (3) / (6)
|
||||
✓ more than two datasets with (1) / (4) / (5)
|
||||
✓ more than two datasets with (1) / (4) / (6)
|
||||
✓ more than two datasets with (2) / (3) / (5)
|
||||
✓ more than two datasets with (2) / (3) / (6)
|
||||
✓ more than two datasets with (2) / (4) / (5)
|
||||
✓ more than two datasets with (2) / (4) / (6)
|
||||
✓ more than two datasets did the job right
|
||||
✓ it can resolve a dataset after the test case is available with (Closure Object (...))
|
||||
✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #1
|
||||
✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #2
|
||||
✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #1
|
||||
✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #2
|
||||
|
||||
PASS Tests\Features\Exceptions
|
||||
✓ it gives access the the underlying expectException
|
||||
✓ it catch exceptions
|
||||
✓ it catch exceptions and messages
|
||||
✓ it can just define the message
|
||||
|
||||
PASS Tests\Features\Expect\HigherOrder\methods
|
||||
✓ it can access methods
|
||||
✓ it can access multiple methods
|
||||
✓ it works with not
|
||||
✓ it can accept arguments
|
||||
✓ it works with each
|
||||
✓ it works inside of each
|
||||
✓ it works with sequence
|
||||
✓ it can compose complex expectations
|
||||
✓ it can handle nested method calls
|
||||
✓ it works with higher order tests
|
||||
|
||||
PASS Tests\Features\Expect\HigherOrder\methodsAndProperties
|
||||
✓ it can access methods and properties
|
||||
✓ it can handle nested methods and properties
|
||||
✓ it works with higher order tests
|
||||
✓ it can start a new higher order expectation using the and syntax
|
||||
✓ it can start a new higher order expectation using the and syntax in higher order tests
|
||||
|
||||
PASS Tests\Features\Expect\HigherOrder\properties
|
||||
✓ it allows properties to be accessed from the value
|
||||
✓ it can access multiple properties from the value
|
||||
✓ it works with not
|
||||
✓ it works with each
|
||||
✓ it works inside of each
|
||||
✓ it works with sequence
|
||||
✓ it can compose complex expectations
|
||||
✓ it works with objects
|
||||
✓ it works with nested properties
|
||||
✓ it works with higher order tests
|
||||
|
||||
PASS Tests\Features\Expect\each
|
||||
✓ an exception is thrown if the the type is not iterable
|
||||
✓ it expects on each item
|
||||
✓ it chains expectations on each item
|
||||
✓ opposite expectations on each item
|
||||
✓ chained opposite and non-opposite expectations
|
||||
✓ it can add expectations via "and"
|
||||
✓ it accepts callables
|
||||
|
||||
PASS Tests\Features\Expect\extend
|
||||
✓ it macros true is true
|
||||
✓ it macros false is not true
|
||||
✓ it macros true is true with argument
|
||||
✓ it macros false is not true with argument
|
||||
|
||||
PASS Tests\Features\Expect\json
|
||||
✓ it properly parses json string
|
||||
✓ fails with broken json string
|
||||
|
||||
PASS Tests\Features\Expect\not
|
||||
✓ not property calls
|
||||
|
||||
PASS Tests\Features\Expect\ray
|
||||
✓ ray calls do not fail when ray is not installed
|
||||
|
||||
PASS Tests\Features\Expect\sequence
|
||||
✓ an exception is thrown if the the type is not iterable
|
||||
✓ allows for sequences of checks to be run on iterable data
|
||||
✓ loops back to the start if it runs out of sequence items
|
||||
✓ it works if the number of items in the iterable is smaller than the number of expectations
|
||||
✓ it works with associative arrays
|
||||
✓ it can be passed non-callable values
|
||||
✓ it can be passed a mixture of value types
|
||||
|
||||
PASS Tests\Features\Expect\toBe
|
||||
✓ strict comparisons
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeArray
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeBool
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeCallable
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeDirectory
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeEmpty
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeFalse
|
||||
✓ strict comparisons
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeFalsy
|
||||
✓ passes as falsy with (false)
|
||||
✓ passes as falsy with ('')
|
||||
✓ passes as falsy with (null)
|
||||
✓ passes as falsy with (0)
|
||||
✓ passes as falsy with ('0')
|
||||
✓ passes as not falsy with (true)
|
||||
✓ passes as not falsy with (1) #1
|
||||
✓ passes as not falsy with ('false')
|
||||
✓ passes as not falsy with (1) #2
|
||||
✓ passes as not falsy with (-1)
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeFile
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeFloat
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeGreatherThan
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeGreatherThanOrEqual
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeIn
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeInfinite
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeInstanceOf
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeInt
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeIterable
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeJson
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeLessThan
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeLessThanOrEqual
|
||||
✓ passes
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeNAN
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeNull
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeNumeric
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeObject
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeReadableDirectory
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeReadableFile
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeResource
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeScalar
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeString
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeTrue
|
||||
✓ strict comparisons
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeTruthy
|
||||
✓ passes as truthy with (true)
|
||||
✓ passes as truthy with (1) #1
|
||||
✓ passes as truthy with ('false')
|
||||
✓ passes as truthy with (1) #2
|
||||
✓ passes as truthy with (-1)
|
||||
✓ passes as not truthy with (false)
|
||||
✓ passes as not truthy with ('')
|
||||
✓ passes as not truthy with (null)
|
||||
✓ passes as not truthy with (0)
|
||||
✓ passes as not truthy with ('0')
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeWritableDirectory
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toBeWritableFile
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toContain
|
||||
✓ passes strings
|
||||
✓ passes strings with multiple needles
|
||||
✓ passes arrays
|
||||
✓ passes arrays with multiple needles
|
||||
✓ passes with array 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)
|
||||
|
||||
PASS Tests\Features\Expect\toEndWith
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toEqual
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toEqualCanonicalizing
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toEqualWithDelta
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toHaveCount
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toHaveKey
|
||||
✓ pass
|
||||
✓ pass with nested key
|
||||
✓ pass with plain key with dots
|
||||
✓ pass with value check
|
||||
✓ pass with value check and nested key
|
||||
✓ pass with value check and plain key with dots
|
||||
✓ failures
|
||||
✓ failures with nested key
|
||||
✓ failures with plain key with dots
|
||||
✓ fails with wrong value
|
||||
✓ fails with wrong value and nested key
|
||||
✓ fails with wrong value and plain key with dots
|
||||
✓ not failures
|
||||
✓ not failures with nested key
|
||||
✓ not failures with plain key with dots
|
||||
✓ not failures with correct value
|
||||
✓ not failures with correct value and with nested key
|
||||
✓ not failures with correct value and with plain key with dots
|
||||
|
||||
PASS Tests\Features\Expect\toHaveKeys
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toHaveProperty
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toMatch
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toMatchArray
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toMatchConstraint
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toMatchObject
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Expect\toStartWith
|
||||
✓ pass
|
||||
✓ failures
|
||||
✓ not failures
|
||||
|
||||
PASS Tests\Features\Helpers
|
||||
✓ it can set/get properties on $this
|
||||
@ -245,9 +455,27 @@
|
||||
✓ it allows to call underlying protected/private methods
|
||||
✓ it throws error if method do not exist
|
||||
✓ it can forward unexpected calls to any global function
|
||||
✓ it can use helpers from helpers file
|
||||
|
||||
PASS Tests\Features\HigherOrderTests
|
||||
✓ it proxies calls to object
|
||||
✓ it is capable doing multiple assertions
|
||||
✓ it resolves expect callables correctly
|
||||
✓ does not treat method names as callables
|
||||
✓ it can tap into the test
|
||||
✓ it can pass datasets into the expect callables with (1, 2, 3)
|
||||
✓ it can pass datasets into the tap callable with (1, 2, 3)
|
||||
✓ it can pass shared datasets into callables with (1)
|
||||
✓ it can pass shared datasets into callables with (2)
|
||||
|
||||
WARN Tests\Features\Incompleted
|
||||
… incompleted
|
||||
… it is incompleted
|
||||
… it is incompleted even with method calls like skip
|
||||
… it is incompleted even with method calls like group
|
||||
✓ it is not incompleted because of expect
|
||||
✓ it is not incompleted because of assert
|
||||
✓ it is not incompleted because of test with assertions
|
||||
|
||||
PASS Tests\Features\It
|
||||
✓ it is a test
|
||||
@ -257,10 +485,8 @@
|
||||
✓ it can call chained macro method
|
||||
✓ it will throw exception from call if no macro exists
|
||||
|
||||
PASS Tests\Features\Mocks
|
||||
✓ it has bar
|
||||
|
||||
PASS Tests\Features\PendingHigherOrderTests
|
||||
✓ get 'foo'
|
||||
✓ get 'foo' → get 'bar' → expect true → toBeTrue
|
||||
✓ get 'foo' → expect true → toBeTrue
|
||||
|
||||
@ -272,6 +498,9 @@
|
||||
- it skips with truthy closure condition
|
||||
✓ it do not skips with falsy closure condition
|
||||
- it skips with condition and message → skipped because foo
|
||||
- it skips when skip after assertion
|
||||
- it can use something in the test case as a condition → This test was skipped
|
||||
- it can user higher order callables and skip
|
||||
|
||||
PASS Tests\Features\Test
|
||||
✓ a test
|
||||
@ -283,6 +512,45 @@
|
||||
PASS Tests\Fixtures\ExampleTest
|
||||
✓ it example 2
|
||||
|
||||
PASS Tests\Hooks\AfterAllTest
|
||||
✓ global afterAll execution order
|
||||
✓ it only gets called once per file
|
||||
|
||||
PASS Tests\Hooks\AfterEachTest
|
||||
✓ global afterEach execution order
|
||||
|
||||
PASS Tests\Hooks\BeforeAllTest
|
||||
✓ global beforeAll execution order
|
||||
✓ it only gets called once per file
|
||||
|
||||
PASS Tests\Hooks\BeforeEachTest
|
||||
✓ global beforeEach execution order
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\InvalidTestName
|
||||
✓ it runs file names like `@#$%^&()-_=+.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\ATestWithSpaces
|
||||
✓ it runs file names like `A Test With Spaces.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\AdditionalFileExtensionspec
|
||||
✓ it runs file names like `AdditionalFileExtension.spec.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\FolderWithAn\ExampleTest
|
||||
✓ custom traits can be used
|
||||
✓ trait applied in this file
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\ManyExtensionsclasstest
|
||||
✓ it runs file names like `ManyExtensions.class.test.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\TestCaseWithQuotes
|
||||
✓ it runs file names like `Test 'Case' With Quotes.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\kebabcasespec
|
||||
✓ it runs file names like `kebab-case-spec.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomAffixes\snakecasespec
|
||||
✓ it runs file names like `snake_case_spec.php`
|
||||
|
||||
PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory
|
||||
✓ closure was bound to CustomTestCase
|
||||
|
||||
@ -313,6 +581,16 @@
|
||||
✓ it throws exception when `process isolation` is true
|
||||
✓ it do not throws exception when `process isolation` is false
|
||||
|
||||
PASS Tests\Unit\Console\Help
|
||||
✓ it outputs the help information when --help is used
|
||||
|
||||
PASS Tests\Unit\Datasets
|
||||
✓ it show only the names of named datasets in their description
|
||||
✓ it show the actual dataset of non-named datasets in their description
|
||||
✓ it show only the names of multiple named datasets in their description
|
||||
✓ it show the actual dataset of multiple non-named datasets in their description
|
||||
✓ it show the correct description for mixed named and not-named datasets
|
||||
|
||||
PASS Tests\Unit\Plugins\Version
|
||||
✓ it outputs the version when --version is used
|
||||
✓ it do not outputs version when --version is not used
|
||||
@ -335,6 +613,15 @@
|
||||
|
||||
PASS Tests\Unit\TestSuite
|
||||
✓ it does not allow to add the same test description twice
|
||||
✓ it alerts users about tests with arguments but no input
|
||||
✓ it can return an array of all test suite filenames
|
||||
✓ it can filter the test suite filenames to those with the only method
|
||||
|
||||
PASS Tests\Visual\Help
|
||||
✓ visual snapshot of help command output
|
||||
|
||||
PASS Tests\Visual\JUnit
|
||||
✓ it is can successfully call all public methods
|
||||
|
||||
PASS Tests\Visual\SingleTestOrDirectory
|
||||
✓ allows to run a single test
|
||||
@ -345,13 +632,22 @@
|
||||
WARN Tests\Visual\Success
|
||||
- visual snapshot of test suite on success
|
||||
|
||||
PASS Tests\Visual\TeamCity
|
||||
✓ it is can successfully call all public methods
|
||||
|
||||
PASS Tests\Features\Depends
|
||||
✓ first
|
||||
✓ second
|
||||
✓ it asserts true is true
|
||||
✓ depends
|
||||
✓ depends with ...params
|
||||
✓ depends with defined arguments
|
||||
✓ depends run test only once
|
||||
✓ depends works with the correct test name
|
||||
|
||||
Tests: 6 skipped, 208 passed
|
||||
PASS Tests\Features\DependsInheritance
|
||||
✓ it is a test
|
||||
✓ it uses correct parent class
|
||||
|
||||
Tests: 4 incompleted, 9 skipped, 421 passed
|
||||
|
||||
11
tests/Datasets/Bound.php
Normal file
11
tests/Datasets/Bound.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
dataset('bound.closure', function () {
|
||||
yield function () { return 1; };
|
||||
yield function () { return 2; };
|
||||
});
|
||||
|
||||
dataset('bound.array', [
|
||||
function () { return 1; },
|
||||
function () { return 2; },
|
||||
]);
|
||||
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
|
||||
test('passes strings', function () {
|
||||
expect([1, 2, 42])->toContain(42);
|
||||
});
|
||||
|
||||
test('passes arrays', function () {
|
||||
expect('Nuno')->toContain('Nu');
|
||||
});
|
||||
|
||||
test('failures', function () {
|
||||
expect([1, 2, 42])->toContain(3);
|
||||
})->throws(ExpectationFailedException::class);
|
||||
|
||||
test('not failures', function () {
|
||||
expect([1, 2, 42])->not->toContain(42);
|
||||
})->throws(ExpectationFailedException::class);
|
||||
@ -6,7 +6,7 @@ beforeEach(function () use ($state) {
|
||||
$this->state = $state;
|
||||
});
|
||||
|
||||
afterEach(function () use ($state) {
|
||||
afterEach(function () {
|
||||
$this->state->bar = 2;
|
||||
});
|
||||
|
||||
|
||||
@ -8,11 +8,11 @@ beforeAll(function () use ($foo) {
|
||||
});
|
||||
|
||||
it('gets executed before tests', function () use ($foo) {
|
||||
expect($foo->bar)->toBe(1);
|
||||
expect($foo)->bar->toBe(1);
|
||||
|
||||
$foo->bar = 'changed';
|
||||
});
|
||||
|
||||
it('do not get executed before each test', function () use ($foo) {
|
||||
expect($foo->bar)->toBe('changed');
|
||||
expect($foo)->bar->toBe('changed');
|
||||
});
|
||||
|
||||
58
tests/Features/Coverage.php
Normal file
58
tests/Features/Coverage.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Pest\Plugins\Coverage as CoveragePlugin;
|
||||
use Pest\Support\Coverage;
|
||||
use Pest\TestSuite;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
|
||||
it('has plugin')->assertTrue(class_exists(CoveragePlugin::class));
|
||||
|
||||
it('adds coverage if --coverage exist', function () {
|
||||
$plugin = new CoveragePlugin(new ConsoleOutput());
|
||||
$testSuite = TestSuite::getInstance();
|
||||
|
||||
expect($plugin->coverage)->toBeFalse();
|
||||
$arguments = $plugin->handleArguments([]);
|
||||
expect($arguments)->toEqual([]);
|
||||
expect($plugin->coverage)->toBeFalse();
|
||||
|
||||
$arguments = $plugin->handleArguments(['--coverage']);
|
||||
expect($arguments)->toEqual(['--coverage-php', Coverage::getPath()]);
|
||||
expect($plugin->coverage)->toBeTrue();
|
||||
});
|
||||
|
||||
it('adds coverage if --min exist', function () {
|
||||
$plugin = new CoveragePlugin(new ConsoleOutput());
|
||||
expect($plugin->coverageMin)->toEqual(0.0);
|
||||
|
||||
expect($plugin->coverage)->toBeFalse();
|
||||
$plugin->handleArguments([]);
|
||||
expect($plugin->coverageMin)->toEqual(0.0);
|
||||
|
||||
$plugin->handleArguments(['--min=2']);
|
||||
expect($plugin->coverageMin)->toEqual(2.0);
|
||||
|
||||
$plugin->handleArguments(['--min=2.4']);
|
||||
expect($plugin->coverageMin)->toEqual(2.4);
|
||||
});
|
||||
|
||||
it('generates coverage based on file input', function () {
|
||||
expect(Coverage::getMissingCoverage(new class() {
|
||||
public function lineCoverageData(): array
|
||||
{
|
||||
return [
|
||||
1 => ['foo'],
|
||||
2 => ['bar'],
|
||||
4 => [],
|
||||
5 => [],
|
||||
6 => [],
|
||||
7 => null,
|
||||
100 => null,
|
||||
101 => ['foo'],
|
||||
102 => [],
|
||||
];
|
||||
}
|
||||
}))->toEqual([
|
||||
'4..6', '102',
|
||||
]);
|
||||
});
|
||||
@ -5,6 +5,10 @@ use Pest\Exceptions\DatasetAlreadyExist;
|
||||
use Pest\Exceptions\DatasetDoesNotExist;
|
||||
use Pest\Plugin;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->foo = 'bar';
|
||||
});
|
||||
|
||||
it('throws exception if dataset does not exist', function () {
|
||||
$this->expectException(DatasetDoesNotExist::class);
|
||||
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
|
||||
@ -95,6 +99,18 @@ test('eager registered wrapped datasets did the job right', function () use ($st
|
||||
expect($state->text)->toBe('1212121212');
|
||||
});
|
||||
|
||||
test('named datasets', function ($text) use ($state, $datasets) {
|
||||
$state->text .= $text;
|
||||
expect($datasets)->toContain([$text]);
|
||||
})->with([
|
||||
'one' => [1],
|
||||
'two' => [2],
|
||||
]);
|
||||
|
||||
test('named datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('121212121212');
|
||||
});
|
||||
|
||||
class Bar
|
||||
{
|
||||
public $name = 1;
|
||||
@ -104,7 +120,7 @@ $namedDatasets = [
|
||||
new Bar(),
|
||||
];
|
||||
|
||||
test('lazy named datasets', function ($text) use ($state, $datasets) {
|
||||
test('lazy named datasets', function ($text) {
|
||||
expect(true)->toBeTrue();
|
||||
})->with($namedDatasets);
|
||||
|
||||
@ -125,3 +141,103 @@ it('creates unique test case names', function (string $name, Plugin $plugin, boo
|
||||
it('creates unique test case names - count', function () use (&$counter) {
|
||||
expect($counter)->toBe(6);
|
||||
});
|
||||
|
||||
$datasets_a = [[1], [2]];
|
||||
$datasets_b = [[3], [4]];
|
||||
|
||||
test('lazy multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
|
||||
$state->text .= $text_a . $text_b;
|
||||
expect($datasets_a)->toContain([$text_a]);
|
||||
expect($datasets_b)->toContain([$text_b]);
|
||||
})->with($datasets_a, $datasets_b);
|
||||
|
||||
test('lazy multiple datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('12121212121213142324');
|
||||
});
|
||||
|
||||
$state->text = '';
|
||||
|
||||
test('eager multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
|
||||
$state->text .= $text_a . $text_b;
|
||||
expect($datasets_a)->toContain([$text_a]);
|
||||
expect($datasets_b)->toContain([$text_b]);
|
||||
})->with(function () use ($datasets_a) {
|
||||
return $datasets_a;
|
||||
})->with(function () use ($datasets_b) {
|
||||
return $datasets_b;
|
||||
});
|
||||
|
||||
test('eager multiple datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('1212121212121314232413142324');
|
||||
});
|
||||
|
||||
test('lazy registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
|
||||
$state->text .= $text_a . $text_b;
|
||||
expect($datasets)->toContain([$text_a]);
|
||||
expect($datasets)->toContain([$text_b]);
|
||||
})->with('numbers.array')->with('numbers.array');
|
||||
|
||||
test('lazy registered multiple datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('121212121212131423241314232411122122');
|
||||
});
|
||||
|
||||
test('eager registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
|
||||
$state->text .= $text_a . $text_b;
|
||||
expect($datasets)->toContain([$text_a]);
|
||||
expect($datasets)->toContain([$text_b]);
|
||||
})->with('numbers.array')->with('numbers.closure');
|
||||
|
||||
test('eager registered multiple datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('12121212121213142324131423241112212211122122');
|
||||
});
|
||||
|
||||
test('eager wrapped registered multiple datasets', function ($text_a, $text_b) use ($state, $datasets) {
|
||||
$state->text .= $text_a . $text_b;
|
||||
expect($datasets)->toContain([$text_a]);
|
||||
expect($datasets)->toContain([$text_b]);
|
||||
})->with('numbers.closure.wrapped')->with('numbers.closure');
|
||||
|
||||
test('eager wrapped registered multiple datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('1212121212121314232413142324111221221112212211122122');
|
||||
});
|
||||
|
||||
test('named multiple datasets', function ($text_a, $text_b) use ($state, $datasets_a, $datasets_b) {
|
||||
$state->text .= $text_a . $text_b;
|
||||
expect($datasets_a)->toContain([$text_a]);
|
||||
expect($datasets_b)->toContain([$text_b]);
|
||||
})->with([
|
||||
'one' => [1],
|
||||
'two' => [2],
|
||||
])->with([
|
||||
'three' => [3],
|
||||
'four' => [4],
|
||||
]);
|
||||
|
||||
test('named multiple datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324');
|
||||
});
|
||||
|
||||
test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state, $datasets_a, $datasets_b) {
|
||||
$state->text .= $text_a . $text_b . $text_c;
|
||||
expect($datasets_a)->toContain([$text_a]);
|
||||
expect($datasets_b)->toContain([$text_b]);
|
||||
expect([5, 6])->toContain($text_c);
|
||||
})->with($datasets_a, $datasets_b)->with([5, 6]);
|
||||
|
||||
test('more than two datasets did the job right', function () use ($state) {
|
||||
expect($state->text)->toBe('121212121212131423241314232411122122111221221112212213142324135136145146235236245246');
|
||||
});
|
||||
|
||||
it('can resolve a dataset after the test case is available', function ($result) {
|
||||
expect($result)->toBe('bar');
|
||||
})->with([
|
||||
function () { return $this->foo; },
|
||||
]);
|
||||
|
||||
it('can resolve a dataset after the test case is available with shared yield sets', function ($result) {
|
||||
expect($result)->toBeInt()->toBeLessThan(3);
|
||||
})->with('bound.closure');
|
||||
|
||||
it('can resolve a dataset after the test case is available with shared array sets', function ($result) {
|
||||
expect($result)->toBeInt()->toBeLessThan(3);
|
||||
})->with('bound.array');
|
||||
|
||||
@ -32,3 +32,7 @@ test('depends with defined arguments', function (string $first, string $second)
|
||||
test('depends run test only once', function () use (&$runCounter) {
|
||||
expect($runCounter)->toBe(2);
|
||||
})->depends('first', 'second');
|
||||
|
||||
// Regression tests. See https://github.com/pestphp/pest/pull/216
|
||||
it('asserts true is true')->assertTrue(true);
|
||||
test('depends works with the correct test name')->assertTrue(true)->depends('it asserts true is true');
|
||||
|
||||
22
tests/Features/DependsInheritance.php
Normal file
22
tests/Features/DependsInheritance.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class InheritanceTest extends TestCase
|
||||
{
|
||||
public function foo()
|
||||
{
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
|
||||
uses(InheritanceTest::class);
|
||||
|
||||
it('is a test', function () {
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
it('uses correct parent class', function () {
|
||||
expect(get_parent_class($this))->toEqual(InheritanceTest::class);
|
||||
expect($this->foo())->toEqual('bar');
|
||||
})->depends('it is a test');
|
||||
@ -13,3 +13,7 @@ it('catch exceptions', function () {
|
||||
it('catch exceptions and messages', function () {
|
||||
throw new Exception('Something bad happened');
|
||||
})->throws(Exception::class, 'Something bad happened');
|
||||
|
||||
it('can just define the message', function () {
|
||||
throw new Exception('Something bad happened');
|
||||
})->throws('Something bad happened');
|
||||
|
||||
120
tests/Features/Expect/HigherOrder/methods.php
Normal file
120
tests/Features/Expect/HigherOrder/methods.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
it('can access methods', function () {
|
||||
expect(new HasMethods())
|
||||
->name()->toBeString()->toEqual('Has Methods');
|
||||
});
|
||||
|
||||
it('can access multiple methods', function () {
|
||||
expect(new HasMethods())
|
||||
->name()->toBeString()->toEqual('Has Methods')
|
||||
->quantity()->toBeInt()->toEqual(20);
|
||||
});
|
||||
|
||||
it('works with not', function () {
|
||||
expect(new HasMethods())
|
||||
->name()->not->toEqual('world')->toEqual('Has Methods')
|
||||
->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull;
|
||||
});
|
||||
|
||||
it('can accept arguments', function () {
|
||||
expect(new HasMethods())
|
||||
->multiply(5, 4)->toBeInt->toEqual(20);
|
||||
});
|
||||
|
||||
it('works with each', function () {
|
||||
expect(new HasMethods())
|
||||
->attributes()->toBeArray->each->not()->toBeNull
|
||||
->attributes()->each(function ($attribute) {
|
||||
$attribute->not->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('works inside of each', function () {
|
||||
expect(new HasMethods())
|
||||
->books()->each(function ($book) {
|
||||
$book->title->not->toBeNull->cost->toBeGreaterThan(19);
|
||||
});
|
||||
});
|
||||
|
||||
it('works with sequence', function () {
|
||||
expect(new HasMethods())
|
||||
->books()->sequence(
|
||||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); },
|
||||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); },
|
||||
);
|
||||
});
|
||||
|
||||
it('can compose complex expectations', function () {
|
||||
expect(new HasMethods())
|
||||
->toBeObject()
|
||||
->name()->toEqual('Has Methods')->not()->toEqual('bar')
|
||||
->quantity()->not->toEqual('world')->toEqual(20)->toBeInt
|
||||
->multiply(3, 4)->not->toBeString->toEqual(12)
|
||||
->attributes()->toBeArray()
|
||||
->books()->toBeArray->each->not->toBeEmpty
|
||||
->books()->sequence(
|
||||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); },
|
||||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); },
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle nested method calls', function () {
|
||||
expect(new HasMethods())
|
||||
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
|
||||
->newInstance()->name()->toEqual('Has Methods')->not->toBeInt
|
||||
->name()->toEqual('Has Methods')
|
||||
->books()->each->toBeArray();
|
||||
});
|
||||
|
||||
it('works with higher order tests')
|
||||
->expect(new HasMethods())
|
||||
->newInstance()->newInstance()->name()->toEqual('Has Methods')->toBeString()
|
||||
->newInstance()->name()->toEqual('Has Methods')->not->toBeArray
|
||||
->name()->toEqual('Has Methods')
|
||||
->books()->each->toBeArray;
|
||||
|
||||
class HasMethods
|
||||
{
|
||||
public function name()
|
||||
{
|
||||
return 'Has Methods';
|
||||
}
|
||||
|
||||
public function quantity()
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
public function multiply($x, $y)
|
||||
{
|
||||
return $x * $y;
|
||||
}
|
||||
|
||||
public function attributes()
|
||||
{
|
||||
return [
|
||||
'name' => $this->name(),
|
||||
'quantity' => $this->quantity(),
|
||||
];
|
||||
}
|
||||
|
||||
public function books()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'title' => 'Foo',
|
||||
'cost' => 20,
|
||||
],
|
||||
[
|
||||
'title' => 'Bar',
|
||||
'cost' => 30,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function newInstance()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
}
|
||||
91
tests/Features/Expect/HigherOrder/methodsAndProperties.php
Normal file
91
tests/Features/Expect/HigherOrder/methodsAndProperties.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
it('can access methods and properties', function () {
|
||||
expect(new HasMethodsAndProperties())
|
||||
->name->toEqual('Has Methods and Properties')->not()->toEqual('bar')
|
||||
->multiply(3, 4)->not->toBeString->toEqual(12)
|
||||
->posts->each(function ($post) {
|
||||
$post->is_published->toBeTrue;
|
||||
})->books()->toBeArray()
|
||||
->posts->toBeArray->each->not->toBeEmpty
|
||||
->books()->sequence(
|
||||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); },
|
||||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); },
|
||||
);
|
||||
});
|
||||
|
||||
it('can handle nested methods and properties', function () {
|
||||
expect(new HasMethodsAndProperties())
|
||||
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
|
||||
->newInstance()->meta->foo->toBeArray()
|
||||
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
|
||||
->newInstance()->books()->toBeArray();
|
||||
});
|
||||
|
||||
it('works with higher order tests')
|
||||
->expect(new HasMethodsAndProperties())
|
||||
->meta->foo->bar->toBeString()->toEqual('baz')->not->toBeInt
|
||||
->newInstance()->meta->foo->toBeArray
|
||||
->newInstance()->multiply(2, 2)->toEqual(4)->not->toEqual(5)
|
||||
->newInstance()->books()->toBeArray();
|
||||
|
||||
it('can start a new higher order expectation using the and syntax', function () {
|
||||
expect(new HasMethodsAndProperties())
|
||||
->toBeInstanceOf(HasMethodsAndProperties::class)
|
||||
->meta->toBeArray
|
||||
->and(['foo' => 'bar'])
|
||||
->toBeArray()
|
||||
->foo->toEqual('bar');
|
||||
|
||||
expect(static::getCount())->toEqual(4);
|
||||
});
|
||||
|
||||
it('can start a new higher order expectation using the and syntax in higher order tests')
|
||||
->expect(new HasMethodsAndProperties())
|
||||
->toBeInstanceOf(HasMethodsAndProperties::class)
|
||||
->meta->toBeArray
|
||||
->and(['foo' => 'bar'])
|
||||
->toBeArray()
|
||||
->foo->toEqual('bar');
|
||||
|
||||
class HasMethodsAndProperties
|
||||
{
|
||||
public $name = 'Has Methods and Properties';
|
||||
|
||||
public $meta = ['foo' => ['bar' => 'baz']];
|
||||
|
||||
public $posts = [
|
||||
[
|
||||
'is_published' => true,
|
||||
'title' => 'Foo',
|
||||
],
|
||||
[
|
||||
'is_published' => true,
|
||||
'title' => 'Bar',
|
||||
],
|
||||
];
|
||||
|
||||
public function books()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'title' => 'Foo',
|
||||
'cost' => 20,
|
||||
],
|
||||
[
|
||||
'title' => 'Bar',
|
||||
'cost' => 30,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function multiply($x, $y)
|
||||
{
|
||||
return $x * $y;
|
||||
}
|
||||
|
||||
public function newInstance()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
}
|
||||
90
tests/Features/Expect/HigherOrder/properties.php
Normal file
90
tests/Features/Expect/HigherOrder/properties.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
it('allows properties to be accessed from the value', function () {
|
||||
expect(['foo' => 1])->foo->toBeInt()->toEqual(1);
|
||||
});
|
||||
|
||||
it('can access multiple properties from the value', function () {
|
||||
expect(['foo' => 'bar', 'hello' => 'world'])
|
||||
->foo->toBeString()->toEqual('bar')
|
||||
->hello->toBeString()->toEqual('world');
|
||||
});
|
||||
|
||||
it('works with not', function () {
|
||||
expect(['foo' => 'bar', 'hello' => 'world'])
|
||||
->foo->not->not->toEqual('bar')
|
||||
->foo->not->toEqual('world')->toEqual('bar')
|
||||
->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull;
|
||||
});
|
||||
|
||||
it('works with each', function () {
|
||||
expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']])
|
||||
->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5)
|
||||
->words->each(function ($word) {
|
||||
$word->toBeString()->not->toBeInt();
|
||||
});
|
||||
});
|
||||
|
||||
it('works inside of each', function () {
|
||||
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
|
||||
->books->each(function ($book) {
|
||||
$book->title->not->toBeNull->cost->toBeGreaterThan(19);
|
||||
});
|
||||
});
|
||||
|
||||
it('works with sequence', function () {
|
||||
expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]])
|
||||
->books->sequence(
|
||||
function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); },
|
||||
function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); },
|
||||
);
|
||||
});
|
||||
|
||||
it('can compose complex expectations', function () {
|
||||
expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]])
|
||||
->toContain('bar')->toBeArray()
|
||||
->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt
|
||||
->foo->not->toEqual('world')->toEqual('bar')
|
||||
->numbers->toBeArray();
|
||||
});
|
||||
|
||||
it('works with objects', function () {
|
||||
expect(new HasProperties())
|
||||
->name->toEqual('foo')->not->toEqual('world')
|
||||
->posts->toHaveCount(2)->each(function ($post) { $post->is_published->toBeTrue(); })
|
||||
->posts->sequence(
|
||||
function ($post) { $post->title->toEqual('Foo'); },
|
||||
function ($post) { $post->title->toEqual('Bar'); },
|
||||
);
|
||||
});
|
||||
|
||||
it('works with nested properties', function () {
|
||||
expect(new HasProperties())
|
||||
->nested->foo->bar->toBeString()->toEqual('baz')
|
||||
->posts->toBeArray()->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('works with higher order tests')
|
||||
->expect(new HasProperties())
|
||||
->nested->foo->bar->toBeString()->toEqual('baz')
|
||||
->posts->toBeArray()->toHaveCount(2);
|
||||
|
||||
class HasProperties
|
||||
{
|
||||
public $name = 'foo';
|
||||
|
||||
public $posts = [
|
||||
[
|
||||
'is_published' => true,
|
||||
'title' => 'Foo',
|
||||
],
|
||||
[
|
||||
'is_published' => true,
|
||||
'title' => 'Bar',
|
||||
],
|
||||
];
|
||||
|
||||
public $nested = [
|
||||
'foo' => ['bar' => 'baz'],
|
||||
];
|
||||
}
|
||||
89
tests/Features/Expect/each.php
Normal file
89
tests/Features/Expect/each.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
use Pest\Expectation;
|
||||
|
||||
test('an exception is thrown if the the type is not iterable', function () {
|
||||
expect('Foobar')->each()->toEqual('Foobar');
|
||||
})->throws(BadMethodCallException::class, 'Expectation value is not iterable.');
|
||||
|
||||
it('expects on each item', function () {
|
||||
expect([1, 1, 1])
|
||||
->each()
|
||||
->toEqual(1);
|
||||
|
||||
expect(static::getCount())->toBe(3); // + 1 assertion
|
||||
|
||||
expect([1, 1, 1])
|
||||
->each
|
||||
->toEqual(1);
|
||||
|
||||
expect(static::getCount())->toBe(7);
|
||||
});
|
||||
|
||||
it('chains expectations on each item', function () {
|
||||
expect([1, 1, 1])
|
||||
->each()
|
||||
->toBeInt()
|
||||
->toEqual(1);
|
||||
|
||||
expect(static::getCount())->toBe(6); // + 1 assertion
|
||||
|
||||
expect([2, 2, 2])
|
||||
->each
|
||||
->toBeInt
|
||||
->toEqual(2);
|
||||
|
||||
expect(static::getCount())->toBe(13);
|
||||
});
|
||||
|
||||
test('opposite expectations on each item', function () {
|
||||
expect([1, 2, 3])
|
||||
->each()
|
||||
->not()
|
||||
->toEqual(4);
|
||||
|
||||
expect(static::getCount())->toBe(3);
|
||||
|
||||
expect([1, 2, 3])
|
||||
->each()
|
||||
->not->toBeString;
|
||||
|
||||
expect(static::getCount())->toBe(7);
|
||||
});
|
||||
|
||||
test('chained opposite and non-opposite expectations', function () {
|
||||
expect([1, 2, 3])
|
||||
->each()
|
||||
->not()
|
||||
->toEqual(4)
|
||||
->toBeInt();
|
||||
|
||||
expect(static::getCount())->toBe(6);
|
||||
});
|
||||
|
||||
it('can add expectations via "and"', function () {
|
||||
expect([1, 2, 3])
|
||||
->each()
|
||||
->toBeInt // + 3
|
||||
->and([4, 5, 6])
|
||||
->each
|
||||
->toBeLessThan(7) // + 3
|
||||
->not
|
||||
->toBeLessThan(3)
|
||||
->toBeGreaterThan(3) // + 3
|
||||
->and('Hello World')
|
||||
->toBeString // + 1
|
||||
->toEqual('Hello World'); // + 1
|
||||
|
||||
expect(static::getCount())->toBe(14);
|
||||
});
|
||||
|
||||
it('accepts callables', function () {
|
||||
expect([1, 2, 3])->each(function ($number) {
|
||||
expect($number)->toBeInstanceOf(Expectation::class);
|
||||
expect($number->value)->toBeInt();
|
||||
$number->toBeInt->not->toBeString;
|
||||
});
|
||||
|
||||
expect(static::getCount())->toBe(12);
|
||||
});
|
||||
29
tests/Features/Expect/extend.php
Normal file
29
tests/Features/Expect/extend.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
expect()->extend('toBeAMacroExpectation', function () {
|
||||
$this->toBeTrue();
|
||||
|
||||
return $this;
|
||||
});
|
||||
|
||||
expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value) {
|
||||
$this->toBe($value);
|
||||
|
||||
return $this;
|
||||
});
|
||||
|
||||
it('macros true is true', function () {
|
||||
expect(true)->toBeAMacroExpectation();
|
||||
});
|
||||
|
||||
it('macros false is not true', function () {
|
||||
expect(false)->not->toBeAMacroExpectation();
|
||||
});
|
||||
|
||||
it('macros true is true with argument', function () {
|
||||
expect(true)->toBeAMacroExpectationWithArguments(true);
|
||||
});
|
||||
|
||||
it('macros false is not true with argument', function () {
|
||||
expect(false)->not->toBeAMacroExpectationWithArguments(true);
|
||||
});
|
||||
14
tests/Features/Expect/json.php
Normal file
14
tests/Features/Expect/json.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
|
||||
test('it properly parses json string', function () {
|
||||
expect('{"name":"Nuno"}')
|
||||
->json()
|
||||
->name
|
||||
->toBe('Nuno');
|
||||
});
|
||||
|
||||
test('fails with broken json string', function () {
|
||||
expect('{":"Nuno"}')->json();
|
||||
})->throws(ExpectationFailedException::class);
|
||||
5
tests/Features/Expect/ray.php
Normal file
5
tests/Features/Expect/ray.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
test('ray calls do not fail when ray is not installed', function () {
|
||||
expect(true)->ray()->toBe(true);
|
||||
});
|
||||
62
tests/Features/Expect/sequence.php
Normal file
62
tests/Features/Expect/sequence.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
test('an exception is thrown if the the type is not iterable', function () {
|
||||
expect('Foobar')->each->sequence();
|
||||
})->throws(BadMethodCallException::class, 'Expectation value is not iterable.');
|
||||
|
||||
test('allows for sequences of checks to be run on iterable data', function () {
|
||||
expect([1, 2, 3])
|
||||
->sequence(
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(1); },
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(2); },
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(3); },
|
||||
);
|
||||
|
||||
expect(static::getCount())->toBe(6);
|
||||
});
|
||||
|
||||
test('loops back to the start if it runs out of sequence items', function () {
|
||||
expect([1, 2, 3, 1, 2, 3, 1, 2])
|
||||
->sequence(
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(1); },
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(2); },
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(3); },
|
||||
);
|
||||
|
||||
expect(static::getCount())->toBe(16);
|
||||
});
|
||||
|
||||
test('it works if the number of items in the iterable is smaller than the number of expectations', function () {
|
||||
expect([1, 2])
|
||||
->sequence(
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(1); },
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(2); },
|
||||
function ($expectation) { $expectation->toBeInt()->toEqual(3); },
|
||||
);
|
||||
|
||||
expect(static::getCount())->toBe(4);
|
||||
});
|
||||
|
||||
test('it works with associative arrays', function () {
|
||||
expect(['foo' => 'bar', 'baz' => 'boom'])
|
||||
->sequence(
|
||||
function ($expectation, $key) { $expectation->toEqual('bar'); $key->toEqual('foo'); },
|
||||
function ($expectation, $key) { $expectation->toEqual('boom'); $key->toEqual('baz'); },
|
||||
);
|
||||
});
|
||||
|
||||
test('it can be passed non-callable values', function () {
|
||||
expect(['foo', 'bar', 'baz'])->sequence('foo', 'bar', 'baz');
|
||||
|
||||
expect(static::getCount())->toBe(3);
|
||||
});
|
||||
|
||||
test('it can be passed a mixture of value types', function () {
|
||||
expect(['foo', 'bar', 'baz'])->sequence(
|
||||
'foo',
|
||||
function ($expectation) { $expectation->toEqual('bar')->toBeString(); },
|
||||
'baz'
|
||||
);
|
||||
|
||||
expect(static::getCount())->toBe(4);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user