Compare commits

...

148 Commits

Author SHA1 Message Date
075c31bc78 release: v1.17.0 2021-08-26 21:17:03 +01:00
2125bf9668 chore: adjusts tests 2021-08-26 21:14:56 +01:00
dbf3c0a8cf Merge pull request #361 from kbond/throw-expectation
Add `toThrow` expectation
2021-08-26 21:02:38 +01:00
eca5f89e59 release: v1.16.0 2021-08-19 17:07:45 +01:00
0b0beac122 Script update 2021-08-16 22:12:13 +01:00
578e97123d Changes -P to -p 2021-08-13 22:09:55 +01:00
01d672d563 Merge pull request #369 from pestphp/parallel
Parallel testing support
2021-08-13 16:00:26 +01:00
490b2d66e5 Fix 2021-08-13 13:41:00 +01:00
0368c4846f Adds a composer test:parallel script 2021-08-13 13:40:07 +01:00
4dfc02c5da Refactor 2021-08-13 11:19:03 +01:00
5c84b0c6d3 The getFilenames method now respects only calls. 2021-08-13 11:07:52 +01:00
b6c06e8c30 Bugfix 2021-08-13 10:46:07 +01:00
c6435d5606 Adds a helpful message for users trying to run parallel without the plugin 2021-08-13 10:44:11 +01:00
2887d212e3 CS fix 2021-08-13 10:30:26 +01:00
cadae52d5d Updates snapshots 2021-08-13 10:29:21 +01:00
d9749ca65b Adds a method for getting all filenames 2021-08-13 10:26:38 +01:00
c2070cd99d Refactor 2021-08-13 10:19:10 +01:00
03d34e9a10 Removes isInParallel 2021-08-13 10:14:01 +01:00
45e76a6df6 Typehint updates 2021-08-13 09:41:45 +01:00
28dd3c2a03 Adds -P as a parallel shortcut 2021-08-13 09:39:56 +01:00
5de981d923 Updates composer.json 2021-08-13 09:12:56 +01:00
a55b31e7c3 Update FUNDING.yml 2021-08-12 21:41:42 +01:00
5f0bd8180e Update FUNDING.yml 2021-08-12 21:41:05 +01:00
e1f1fcccbe Removes unneeded dependency 2021-08-11 21:25:41 +01:00
ab04aef561 Refactors addOutput 2021-08-11 20:59:19 +01:00
79ddb1f58e Composer change 2021-08-11 15:57:49 +01:00
c7a2e68941 Fixes a problem with the PhpUnit logger 2021-08-10 14:57:36 +01:00
5c592928d4 Adds a new method, isInParallel, to the Testable trait to allow a test to determine its parallel status. 2021-08-10 14:34:10 +01:00
bcab4224fb Skips a test that doesn't support parallel. 2021-08-10 14:24:34 +01:00
892f70b5b5 Refactor 2021-08-10 13:17:11 +01:00
5c7de5ad75 Refactor 2021-08-10 12:04:15 +01:00
995088b522 Refactor 2021-08-10 11:32:43 +01:00
ef503646ee Removes parallel classes. 2021-08-10 11:30:48 +01:00
a760470e48 Adds Pest output to parallel. 2021-08-09 18:57:20 +01:00
31d1b1b91d Adds a little more spacing above the coverage output. 2021-08-06 15:15:09 +01:00
7524c80af6 Merge pull request #370 from nuernbergerA/parallel
Parallel additions
2021-08-06 15:04:51 +01:00
721d5134b7 replace str_starts_with to support pre php 8.0 2021-08-06 16:00:03 +02:00
0b5321fdd7 only check for coverage driver if option is present 2021-08-06 15:48:38 +02:00
c86058fed1 use support class 2021-08-06 15:39:19 +02:00
8b295b5e9d remove debug statements 2021-08-06 15:38:59 +02:00
221248e691 introduced argument mapping, added pest coverage 2021-08-06 15:25:28 +02:00
7621247bb7 Adds workflows for parallel 2021-08-05 17:45:50 +01:00
463a50ebd4 Investigating bug fix 2021-08-05 17:39:23 +01:00
62aabc6ae1 Bugfix. The TestCase is now aware of if it is running in parallel or not 😎 2021-08-05 17:09:45 +01:00
1ca9aa5ca6 Further cleanup 2021-08-05 16:43:09 +01:00
7a76f8dce2 Further cleanup 2021-08-05 16:21:06 +01:00
beca27599c Further cleanup 2021-08-05 16:18:47 +01:00
256b167eaf Improvements 2021-08-05 15:31:09 +01:00
5526d4c24d Cleanup 2021-08-05 13:51:55 +01:00
7ea138c640 CS fixes 2021-08-05 13:41:04 +01:00
c4a659c3b5 Composer changes 2021-08-05 13:16:29 +01:00
0a3991c314 Initial working draft 2021-08-05 13:13:53 +01:00
d1a9e0bbe3 release: v1.15.0 2021-08-04 23:02:18 +01:00
17eacfdf95 Update RELEASE.md 2021-08-04 23:01:40 +01:00
9ec0762d41 tests: refactors toBeTruthy and toBeFalsy 2021-08-04 22:56:18 +01:00
30f39f1850 Merge pull request #367 from gabbanaesteban/add-to-be-falsy-and-to-be-truthy
Add `toBeTruthy` and `ToBeFalsy`
2021-08-04 22:45:39 +01:00
8ee07330b3 Add toBeFalsy 2021-08-03 20:55:07 -04:00
cffde4564d Add toBeTruthy 2021-08-03 20:54:56 -04:00
ce7a7649a2 chore: removes scripts folder 2021-08-03 18:59:36 +01:00
eeed7e6a0a chore: removes scripts folder 2021-08-03 18:57:41 +01:00
d6844f5239 Delete compile.php 2021-08-03 18:55:53 +01:00
06c4019e81 cs: removes comment 2021-08-03 18:22:15 +01:00
7785a8cc58 chore: reverts changes regarding PHP 8.1 2021-08-03 18:00:42 +01:00
663516c1e3 Merge pull request #366 from jeroenvanrensen/patch-1
Fix typography
2021-08-03 14:32:50 +01:00
e83667a20b docs: update changelog 2021-08-03 14:01:53 +01:00
aa96d75fb9 Merge pull request #364 from pestphp/bound-datasets
Datasets can now access the test case
2021-08-03 13:58:35 +01:00
a5af4bc5ed Fix typography 2021-08-03 15:04:03 +03:00
57161ba5ad Merges master 2021-08-02 14:03:33 +01:00
b1a9254fc1 Merge branch 'master' into bound-datasets
# Conflicts:
#	tests/.snapshots/success.txt
2021-08-02 14:02:49 +01:00
e56e818659 Splits 8.1 OS jobs 2021-08-02 13:06:41 +01:00
79de0d5875 Splits 8.1 OS jobs 2021-08-02 13:05:55 +01:00
f6f8140ebc Splits 8.1 OS jobs 2021-08-02 13:05:29 +01:00
eed221af46 Adds continue-on-error for php 8.1 2021-08-02 13:03:13 +01:00
524457a4e6 Adds continue-on-error for php 8.1 2021-08-02 13:00:07 +01:00
8e289b7a7d Adds continue-on-error for php 8.1 2021-08-02 12:59:07 +01:00
172b69cf15 Adds continue-on-error for php 8.1 2021-08-02 12:57:57 +01:00
475279a4fa Adds continue-on-error for php 8.1 2021-08-02 12:56:29 +01:00
4b236bf9ff docs: update changelog 2021-08-02 12:50:44 +01:00
50e2d10029 docs: update changelog 2021-08-02 12:50:28 +01:00
fa8a57f1ab Merge pull request #350 from pestphp/teamcity-styling
Teamcity styling
2021-08-02 12:47:54 +01:00
715f8b420b Updates phpunit dependency version 2021-08-02 12:41:23 +01:00
c776bcf86d add toThrow expectation 2021-08-01 12:33:09 -04:00
a0637d86ff Refactors 2021-08-01 17:00:01 +01:00
1a941d7f92 Refactors 2021-08-01 16:45:06 +01:00
b5959aa3fa Refactors 2021-08-01 15:42:52 +01:00
8861dd2401 Merges with master 2021-08-01 15:40:34 +01:00
da73f4b395 Merge branch 'master' into teamcity-styling
# Conflicts:
#	tests/.snapshots/success.txt
2021-08-01 15:36:53 +01:00
d5097d0fe5 Merge conflicts 2021-07-31 23:28:23 +01:00
022ad4be0d Merge branch 'master' into bound-datasets
# Conflicts:
#	tests/.snapshots/success.txt
2021-07-31 23:27:38 +01:00
2d2a83e9e8 chore: run tests against php 8.1 2021-07-31 22:56:33 +01:00
67c7bee4fa Merge pull request #365 from def-studio/expect-toContain-with-multiple-needles
implements multiple needles in expect()->toContain()
2021-07-31 20:34:33 +01:00
675b0f1ec8 implements multiple needles in expect()->toContain() 2021-07-31 18:29:03 +02:00
c2b86c3ab3 Removes pest from stack traces 2021-07-30 16:34:30 +01:00
46337b8085 Removes pest from stack traces 2021-07-30 16:32:00 +01:00
df172d8eed Adds new lines 2021-07-30 15:36:38 +01:00
24b9160b79 Removes new line from event printing 2021-07-30 15:34:31 +01:00
a7860b0b8e CS 2021-07-30 12:41:57 +01:00
7471c224fa Output improvements. 2021-07-30 12:39:20 +01:00
2996135155 Output improvements. 2021-07-30 12:38:13 +01:00
1abab8d440 Output improvements. 2021-07-30 12:36:52 +01:00
e52c83e5be Output improvements. 2021-07-30 12:35:20 +01:00
5ed4545737 Output improvements. 2021-07-30 12:34:10 +01:00
8b39df68ce Output improvements. 2021-07-30 12:32:27 +01:00
04d8a3762b Output improvements. 2021-07-30 12:31:03 +01:00
0f7c8d00d6 Output improvements. 2021-07-30 12:29:39 +01:00
12fb4f8639 Output improvements. 2021-07-30 12:24:23 +01:00
6a84b825e6 Output improvements. 2021-07-30 12:23:24 +01:00
e8595c56b3 Merge branch 'master' into teamcity-styling 2021-07-30 11:46:57 +01:00
4a9fb2fa74 Snapshot update 2021-07-28 10:41:47 +01:00
d8fae6d689 Datasets can now access the test case and are executed after the setup method has run. 2021-07-28 10:39:39 +01:00
252f9a0e46 Merge pull request #321 from jordanbrauer/fix-missing-dataset-errors
Add user-friendly exception message for missing test inputs
2021-07-28 09:57:39 +01:00
8d24b4a217 chore: replace prop set/check with method call 2021-07-27 23:39:26 -05:00
43920f79a9 feat: add count method for checking message type presence 2021-07-27 23:36:09 -05:00
55bfc5856b Merge branch 'master' into fix-missing-dataset-errors 2021-07-27 23:02:12 -05:00
cd9d4acbc2 release: v1.13.0 2021-07-28 02:03:24 +01:00
2b5355419a chore: fixes types 2021-07-28 01:59:47 +01:00
22b822ce87 fix: skip with a false condition being ignored 2021-07-28 01:57:13 +01:00
b2c298b926 Merge pull request #363 from freekmurze/add-to-be-in
Add `toBeIn` expectation
2021-07-28 00:09:54 +01:00
671f3df115 fix tests 2021-07-28 01:00:19 +02:00
2dd77001b7 static analysis fix 2021-07-28 00:42:54 +02:00
5f574ded81 add toBeIn 2021-07-28 00:36:43 +02:00
6309e6818d Update README.md 2021-07-26 23:19:51 +01:00
4813ab6ffb Add sponsor 2021-07-26 23:18:55 +01:00
00b4bb6305 Merge branch 'master' into fix-missing-dataset-errors 2021-07-21 21:26:33 -05:00
d10281f851 Adds test cases for loggers and removes use of str_starts_with. 2021-07-19 12:59:18 +01:00
05f72f9b6d Aligns test case name with test names 2021-07-16 17:59:07 +01:00
f9de1b9c00 Refactor 2021-07-16 17:51:12 +01:00
9516e56242 Improves test case name styling 2021-07-16 17:50:18 +01:00
5f315fc899 Updates warning color 2021-07-16 17:46:30 +01:00
e59606818d More work on output 2021-07-16 17:26:48 +01:00
e16104350e More work on output 2021-07-16 16:45:35 +01:00
df31191f4e Merge branch 'master' into teamcity-assertion-fix 2021-07-16 16:45:16 +01:00
5b310f6f93 More work on output 2021-07-16 15:58:44 +01:00
0200f90e9a Start of work on better TeamCity output 2021-07-16 15:06:10 +01:00
dc75b34deb refactor: move logic into boolean method in TestCaseFactory 2021-06-30 09:51:49 -05:00
8e22803797 Merge branch 'master' into fix-missing-dataset-errors 2021-06-30 09:04:20 -05:00
82bd836ae9 Merge branch 'master' into fix-missing-dataset-errors 2021-06-21 19:50:15 -05:00
e64856c664 test: use throws method instead of assert 2021-06-17 13:33:11 -05:00
de86598c0d Merge branch 'master' into fix-missing-dataset-errors 2021-06-16 21:36:42 -05:00
553b45306f feat: handle unions (PHP 8) 2021-06-16 21:29:08 -05:00
2e7192ab95 chore: static analysis adjustments - deprecation RE: ReflectionType::__toString 2021-06-16 14:42:51 -05:00
e21e3080e0 test: add new exception check & update snapshots 2021-06-16 14:04:50 -05:00
6a7ee90ff5 feat: throw user-friendly exception for missing argument data 2021-06-16 01:01:34 -05:00
9d66893d5a feat: add boolean property to signal dependency proxy calls 2021-06-16 01:00:23 -05:00
64e780cf72 fix: types 2021-06-16 00:58:46 -05:00
9bf141f698 style: formatting & linting 2021-06-16 00:54:22 -05:00
9904094590 feat: add new exception for missing datasets on tests with arguments 2021-06-16 00:53:17 -05:00
9e5b779abc feat: add helper function for mapping a function's arguments 2021-06-16 00:52:31 -05:00
41 changed files with 978 additions and 232 deletions

View File

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

View File

@ -3,7 +3,6 @@
$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-fixer.dist.php']);

View File

@ -4,6 +4,30 @@ 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/).
## [v1.17.0 (2021-08-26)](https://github.com/pestphp/pest/compare/v1.16.0...v1.17.0)
### Added
- `toThrow` expectation ([#361](https://github.com/pestphp/pest/pull/361))
## [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))

View File

@ -22,8 +22,9 @@ We would like to extend our thanks to the following sponsors for funding Pest de
### Premium Sponsors
- **[Akaunting](https://akaunting.com)**
- **[Scout APM](https://scoutapm.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)**.

View File

@ -6,7 +6,7 @@ When releasing a new version of Pest there are some checks and updates that need
- 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 "docs: update changelog"`
- 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`

View File

@ -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;
@ -27,7 +27,7 @@ use Symfony\Component\Console\Output\OutputInterface;
(new Provider())->register();
// get $rootPath based on $autoloadPath
// Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2);
$argv = new ArgvInput();
@ -42,14 +42,22 @@ use Symfony\Component\Console\Output\OutputInterface;
ValidatesEnvironment::in($testSuite);
// lets remove any arguments that PHPUnit does not understand
$args = $_SERVER['argv'];
// Let's remove any arguments that PHPUnit does not understand
if ($argv->hasParameterOption('--test-directory')) {
foreach ($_SERVER['argv'] as $key => $value) {
foreach ($args as $key => $value) {
if (strpos($value, '--test-directory') !== false) {
unset($_SERVER['argv'][$key]);
unset($args[$key]);
}
}
}
exit($container->get(Command::class)->run($_SERVER['argv']));
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));
})();

View File

@ -20,7 +20,7 @@
"php": "^7.3 || ^8.0",
"nunomaduro/collision": "^5.4.0",
"pestphp/pest-plugin": "^1.0.0",
"phpunit/phpunit": "^9.3.7"
"phpunit/phpunit": "^9.5.5"
},
"autoload": {
"psr-4": {
@ -43,7 +43,8 @@
"illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master"
"pestphp/pest-dev-tools": "dev-master",
"pestphp/pest-plugin-parallel": "^1.0"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -57,14 +58,16 @@
"scripts": {
"lint": "php-cs-fixer fix -v",
"test:lint": "php-cs-fixer fix -v --dry-run",
"test:types": "phpstan analyse --ansi --memory-limit=0",
"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"
]
},

View File

@ -7,7 +7,6 @@ parameters:
level: max
paths:
- src
- scripts
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: false

View File

@ -1,37 +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']);
/* @phpstan-ignore-next-line */
@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);

View File

@ -30,7 +30,7 @@ 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.

View File

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

View 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;
}
}

View 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();
}
}

View File

@ -273,7 +273,19 @@ trait Testable
*/
public function __test()
{
return $this->__callClosure($this->__test, func_get_args());
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);
}
/**

View File

@ -6,11 +6,9 @@ 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;
@ -57,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);
@ -128,16 +115,7 @@ final class Command extends BaseCommand
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);
}

View 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,
));
}
}

View File

@ -5,13 +5,19 @@ declare(strict_types=1);
namespace Pest;
use BadMethodCallException;
use Closure;
use InvalidArgumentException;
use Pest\Concerns\Extendable;
use Pest\Concerns\RetrievesValues;
use Pest\Support\Arr;
use Pest\Support\NullClosure;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\ExpectationFailedException;
use ReflectionFunction;
use ReflectionNamedType;
use SebastianBergmann\Exporter\Exporter;
use Throwable;
/**
* @internal
@ -205,6 +211,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.
*/
@ -215,6 +231,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.
*
@ -266,15 +292,17 @@ 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;
}
@ -371,6 +399,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.
*/
@ -715,6 +755,56 @@ final class Expectation
return $this;
}
/**
* Asserts that executing value throws an exception.
*
* @param (Closure(Throwable): mixed)|string $exception
*/
public function toThrow($exception, string $exceptionMessage = null): Expectation
{
$callback = NullClosure::create();
if ($exception instanceof Closure) {
$callback = $exception;
$parameters = (new ReflectionFunction($exception))->getParameters();
if (1 !== count($parameters)) {
throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.');
}
if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) {
throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.');
}
$exception = $type->getName();
}
try {
($this->value)();
} catch (Throwable $e) { // @phpstan-ignore-line
if (!class_exists($exception)) {
Assert::assertStringContainsString($exception, $e->getMessage());
return $this;
}
if ($exceptionMessage !== null) {
Assert::assertStringContainsString($exceptionMessage, $e->getMessage());
}
Assert::assertInstanceOf($exception, $e);
$callback($e);
return $this;
}
if (!class_exists($exception)) {
throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown.");
}
throw new ExpectationFailedException("Exception \"{$exception}\" not thrown.");
}
/**
* Exports the given value.
*

View File

@ -228,4 +228,13 @@ final class TestCaseFactory
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;
}
}

View File

@ -5,27 +5,34 @@ 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\Runner\PhptTestCase;
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;
@ -36,146 +43,97 @@ final class TeamCity extends DefaultResultPrinter
/** @var \PHPUnit\Util\Log\TeamCity */
private $phpunitTeamCity;
public function __construct(bool $verbose, string $colors)
/**
* @param resource|string|null $out
*/
public function __construct($out, 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
);
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->printHeader($result);
$this->printFooter($result);
$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(
'testCount',
['count' => $suite->count()]
);
$this->printEvent(self::TEST_COUNT, [
'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,
$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()),
]);
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|Testable $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|Testable $test
*/
public function endTest(Test $test, float $time): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->endTest($test, $time);
return;
}
if ($test instanceof TestCase) {
$this->numAssertions += $test->getNumAssertions();
} elseif ($test instanceof PhptTestCase) {
$this->numAssertions++;
}
$this->printEvent('testFinished', [
self::NAME => $test->getName(),
self::DURATION => self::toMilliseconds($time),
]);
}
/**
* @param Test|Testable $test
*/
public function addError(Test $test, Throwable $t, float $time): void
{
$this->phpunitTeamCity->addError($test, $t, $time);
}
/**
* @phpstan-ignore-next-line
*
* @param Test|Testable $test
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
$this->phpunitTeamCity->addWarning($test, $e, $time);
}
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->phpunitTeamCity->addFailure($test, $e, $time);
}
protected function writeProgress(string $progress): void
{
}
/**
@ -183,7 +141,7 @@ final class TeamCity extends DefaultResultPrinter
*/
private function printEvent(string $eventName, array $params = []): void
{
$this->write("\n##teamcity[{$eventName}");
$this->write("##teamcity[{$eventName}");
if ($this->flowId !== 0) {
$params['flowId'] = $this->flowId;
@ -206,9 +164,56 @@ final class TeamCity extends DefaultResultPrinter
);
}
private static function toMilliseconds(float $time): int
/** @phpstan-ignore-next-line */
public function endTestSuite(TestSuite $suite): void
{
return (int) round($time * 1000);
$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
@ -218,4 +223,75 @@ final class TeamCity extends DefaultResultPrinter
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);
}
}

View File

@ -148,6 +148,9 @@ final class TestCall
? $conditionOrMessage
: $message;
/** @var callable(): bool $condition */
$condition = $condition->bindTo(null);
$this->testCaseFactory
->chains
->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);

View File

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

View File

@ -5,11 +5,13 @@ 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;
@ -20,7 +22,7 @@ use PHPUnit\Framework\TestCase;
final class TestRepository
{
/**
* @var string
* @var non-empty-string
*/
private const SEPARATOR = '>>>';
@ -42,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.
*/
@ -83,9 +99,7 @@ final class TestRepository
}
}
$onlyState = array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
$onlyState = $this->testsUsingOnly();
$state = count($onlyState) > 0 ? $onlyState : $this->state;
@ -98,6 +112,18 @@ 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`.
*
@ -140,6 +166,14 @@ final class TestRepository
throw new TestAlreadyExist($test->filename, $test->description);
}
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;
}
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ use ReflectionException;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
/**
* @internal
@ -170,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;
}
}

View File

@ -96,11 +96,17 @@
✓ 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
@ -112,11 +118,14 @@
✓ 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
@ -128,6 +137,7 @@
✓ 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
@ -196,6 +206,20 @@
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
@ -216,6 +240,11 @@
PASS Tests\Features\Expect\toBeGreatherThanOrEqual
✓ passes
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeIn
✓ passes
✓ failures
✓ not failures
PASS Tests\Features\Expect\toBeInfinite
@ -301,6 +330,20 @@
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
@ -315,9 +358,16 @@
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
@ -399,6 +449,19 @@
✓ failures
✓ not failures
PASS Tests\Features\Expect\toThrow
✓ passes
✓ failures 1
✓ failures 2
✓ failures 3
✓ failures 4
✓ failures 5
✓ failures 6
✓ failures 7
✓ not failures
✓ closure missing parameter
✓ closure missing type-hint
PASS Tests\Features\Helpers
✓ it can set/get properties on $this
✓ it throws error if property do not exist
@ -411,7 +474,12 @@
✓ 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
@ -444,6 +512,8 @@
✓ 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
@ -457,12 +527,14 @@
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
@ -554,10 +626,16 @@
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
✓ allows to run a directory
@ -567,6 +645,9 @@
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
@ -581,5 +662,5 @@
✓ it is a test
✓ it uses correct parent class
Tests: 4 incompleted, 7 skipped, 365 passed
Tests: 4 incompleted, 9 skipped, 432 passed

11
tests/Datasets/Bound.php Normal file
View 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; },
]);

View File

@ -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']);`.");
@ -223,3 +227,17 @@ test('more than two datasets', function ($text_a, $text_b, $text_c) use ($state,
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');

View File

@ -0,0 +1,19 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes as falsy', function ($value) {
expect($value)->toBeFalsy();
})->with([false, '', null, 0, '0']);
test('passes as not falsy', function ($value) {
expect($value)->not->toBeFalsy();
})->with([true, [1], 'false', 1, -1]);
test('failures', function () {
expect(1)->toBeFalsy();
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(null)->not->toBeFalsy();
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,16 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect('a')->toBeIn(['a', 'b', 'c']);
expect('d')->not->toBeIn(['a', 'b', 'c']);
});
test('failures', function () {
expect('d')->toBeIn(['a', 'b', 'c']);
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect('a')->not->toBeIn(['a', 'b', 'c']);
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,19 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes as truthy', function ($value) {
expect($value)->toBeTruthy();
})->with([true, [1], 'false', 1, -1]);
test('passes as not truthy', function ($value) {
expect($value)->not->toBeTruthy();
})->with([false, '', null, 0, '0']);
test('failures', function () {
expect(null)->toBeTruthy();
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(1)->not->toBeTruthy();
})->throws(ExpectationFailedException::class);

View File

@ -3,17 +3,45 @@
use PHPUnit\Framework\ExpectationFailedException;
test('passes strings', function () {
expect([1, 2, 42])->toContain(42);
expect('Nuno')->toContain('Nu');
});
test('passes strings with multiple needles', function () {
expect('Nuno')->toContain('Nu', 'no');
});
test('passes arrays', function () {
expect('Nuno')->toContain('Nu');
expect([1, 2, 42])->toContain(42);
});
test('passes arrays with multiple needles', function () {
expect([1, 2, 42])->toContain(42, 2);
});
test('passes with array needles', function () {
expect([[1, 2, 3], 2, 42])->toContain(42, [1, 2, 3]);
});
test('failures', function () {
expect([1, 2, 42])->toContain(3);
})->throws(ExpectationFailedException::class);
test('failures with multiple needles (all failing)', function () {
expect([1, 2, 42])->toContain(3, 4);
})->throws(ExpectationFailedException::class);
test('failures with multiple needles (some failing)', function () {
expect([1, 2, 42])->toContain(1, 3, 4);
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect([1, 2, 42])->not->toContain(42);
})->throws(ExpectationFailedException::class);
test('not failures with multiple needles (all failing)', function () {
expect([1, 2, 42])->not->toContain(42, 2);
})->throws(ExpectationFailedException::class);
test('not failures with multiple needles (some failing)', function () {
expect([1, 2, 42])->not->toContain(42, 1);
})->throws(ExpectationFailedException::class);

View File

@ -0,0 +1,60 @@
<?php
use PHPUnit\Framework\ExpectationFailedException;
test('passes', function () {
expect(function () { throw new RuntimeException(); })->toThrow(RuntimeException::class);
expect(function () { throw new RuntimeException(); })->toThrow(Exception::class);
expect(function () { throw new RuntimeException(); })->toThrow(function (RuntimeException $e) {});
expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (Exception $e) {
expect($e->getMessage())->toBe('actual message');
});
expect(function () {})->not->toThrow(Exception::class);
expect(function () { throw new RuntimeException('actual message'); })->toThrow('actual message');
expect(function () { throw new Exception(); })->not->toThrow(RuntimeException::class);
expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'actual message');
expect(function () { throw new RuntimeException('actual message'); })->toThrow(function (RuntimeException $e) {}, 'actual message');
});
test('failures 1', function () {
expect(function () {})->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.');
test('failures 2', function () {
expect(function () {})->toThrow(function (RuntimeException $e) {});
})->throws(ExpectationFailedException::class, 'Exception "' . RuntimeException::class . '" not thrown.');
test('failures 3', function () {
expect(function () { throw new Exception(); })->toThrow(function (RuntimeException $e) {});
})->throws(ExpectationFailedException::class, 'Failed asserting that Exception Object');
test('failures 4', function () {
expect(function () { throw new Exception('actual message'); })
->toThrow(function (Exception $e) {
expect($e->getMessage())->toBe('expected message');
});
})->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical');
test('failures 5', function () {
expect(function () { throw new Exception('actual message'); })->toThrow('expected message');
})->throws(ExpectationFailedException::class, 'Failed asserting that \'actual message\' contains "expected message".');
test('failures 6', function () {
expect(function () {})->toThrow('actual message');
})->throws(ExpectationFailedException::class, 'Exception with message "actual message" not thrown');
test('failures 7', function () {
expect(function () { throw new RuntimeException('actual message'); })->toThrow(RuntimeException::class, 'expected message');
})->throws(ExpectationFailedException::class);
test('not failures', function () {
expect(function () { throw new RuntimeException(); })->not->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class);
test('closure missing parameter', function () {
expect(function () {})->toThrow(function () {});
})->throws(InvalidArgumentException::class, 'The given closure must have a single parameter type-hinted as the class string.');
test('closure missing type-hint', function () {
expect(function () {})->toThrow(function ($e) {});
})->throws(InvalidArgumentException::class, 'The given closure\'s parameter must be type-hinted as the class string.');

View File

@ -7,7 +7,7 @@ global $globalHook;
// HACK: we have to determine our $globalHook->calls baseline. This is because
// two other tests are executed before this one due to filename ordering.
$args = $_SERVER['argv'] ?? [];
$single = isset($args[1]) && Str::endsWith(__FILE__, $args[1]);
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
$offset = $single ? 0 : 2;
uses()->beforeAll(function () use ($globalHook, $offset) {

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Tests\SubFolder\SubFolder\SubFolder;
namespace Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder;
use PHPUnit\Framework\TestCase;

View File

@ -1,3 +0,0 @@
<?php
uses(Tests\SubFolder\SubFolder\SubFolder\CustomTestCaseInSubFolder::class)->in('CustomTestCaseInSubFolders/SubFolder');

View File

@ -1,5 +1,9 @@
<?php
use Tests\CustomTestCaseInSubFolders\SubFolder\SubFolder\CustomTestCaseInSubFolder;
uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder');
uses()->group('integration')->in('Visual');
// NOTE: global test value container to be mutated and checked across files, as needed

View File

@ -1,5 +1,6 @@
<?php
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\TestAlreadyExist;
use Pest\TestSuite;
@ -7,7 +8,44 @@ it('does not allow to add the same test description twice', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
$this->expectException(TestAlreadyExist::class);
$this->expectExceptionMessage(sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__));
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
})->throws(
TestAlreadyExist::class,
sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__),
);
it('alerts users about tests with arguments but no input', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function (int $arg) {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
})->throws(
DatasetMissing::class,
sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__),
);
it('can return an array of all test suite filenames', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test));
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
__FILE__,
]);
});
it('can filter the test suite filenames to those with the only method', function () {
$testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testWithOnly = new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test);
$testWithOnly->only = true;
$testSuite->tests->set($testWithOnly);
$testSuite->tests->set(new \Pest\Factories\TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test));
expect($testSuite->tests->getFilenames())->toEqual([
__FILE__,
]);
});

29
tests/Visual/JUnit.php Normal file
View File

@ -0,0 +1,29 @@
<?php
use Pest\Logging\JUnit;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
beforeEach(function () {
file_put_contents(__DIR__ . '/junit.html', '');
});
it('is can successfully call all public methods', function () {
$junit = new JUnit(__DIR__ . '/junit.html');
$junit->startTestSuite(new TestSuite());
$junit->startTest($this);
$junit->addError($this, new Exception(), 0);
$junit->addFailure($this, new AssertionFailedError(), 0);
$junit->addWarning($this, new Warning(), 0);
$junit->addIncompleteTest($this, new Exception(), 0);
$junit->addRiskyTest($this, new Exception(), 0);
$junit->addSkippedTest($this, new Exception(), 0);
$junit->endTest($this, 0);
$junit->endTestSuite(new TestSuite());
$this->expectNotToPerformAssertions();
});
afterEach(function () {
unlink(__DIR__ . '/junit.html');
});

View File

@ -9,7 +9,7 @@ test('visual snapshot of test suite on success', function () {
]);
$output = function () use ($testsPath) {
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false]));
$process = (new Symfony\Component\Process\Process(['php', 'bin/pest'], dirname($testsPath), ['EXCLUDE' => 'integration', 'REBUILD_SNAPSHOTS' => false, 'PARATEST' => 0]));
$process->run();

32
tests/Visual/TeamCity.php Normal file
View File

@ -0,0 +1,32 @@
<?php
use Pest\Logging\TeamCity;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\DefaultResultPrinter;
beforeEach(function () {
file_put_contents(__DIR__ . '/output.txt', '');
});
it('is can successfully call all public methods', function () {
$teamCity = new TeamCity(__DIR__ . '/output.txt', false, DefaultResultPrinter::COLOR_ALWAYS);
expect($teamCity::isPestTest($this))->toBeTrue();
$teamCity->startTestSuite(new TestSuite());
$teamCity->startTest($this);
$teamCity->addError($this, new Exception('Don\'t worry about this error. Its purposeful.'), 0);
$teamCity->addFailure($this, new AssertionFailedError('Don\'t worry about this error. Its purposeful.'), 0);
$teamCity->addWarning($this, new Warning(), 0);
$teamCity->addIncompleteTest($this, new Exception(), 0);
$teamCity->addRiskyTest($this, new Exception(), 0);
$teamCity->addSkippedTest($this, new Exception(), 0);
$teamCity->endTest($this, 0);
$teamCity->printResult(new TestResult());
$teamCity->endTestSuite(new TestSuite());
});
afterEach(function () {
unlink(__DIR__ . '/output.txt');
});