mirror of
https://github.com/pestphp/pest.git
synced 2026-03-05 23:37:22 +01:00
first
This commit is contained in:
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
; This file is for unifying the coding style for different editors and IDEs.
|
||||||
|
; More information at http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_size = 2
|
||||||
16
.gitattributes
vendored
Normal file
16
.gitattributes
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/art export-ignore
|
||||||
|
/docs export-ignore
|
||||||
|
/tests export-ignore
|
||||||
|
/scripts export-ignore
|
||||||
|
/.github export-ignore
|
||||||
|
/.php_cs 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
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
CONTRIBUTING.md export-ignore
|
||||||
|
README.md export-ignore
|
||||||
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: nunomaduro
|
||||||
|
patreon: nunomaduro
|
||||||
|
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L
|
||||||
51
.github/workflows/ci.yml
vendored
Normal file
51
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on: ['push', 'pull_request']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
php: [7.3, 7.4]
|
||||||
|
dependency-version: [prefer-lowest, prefer-stable]
|
||||||
|
|
||||||
|
name: CI - PHP ${{ matrix.php }} (${{ matrix.dependency-version }})
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.composer/cache/files
|
||||||
|
key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: mbstring, zip
|
||||||
|
tools: prestissimo
|
||||||
|
coverage: pcov
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist
|
||||||
|
|
||||||
|
- name: Coding Style Checks
|
||||||
|
run: |
|
||||||
|
vendor/bin/rector process src --dry-run
|
||||||
|
vendor/bin/php-cs-fixer fix -v --dry-run
|
||||||
|
|
||||||
|
- name: Type Checks
|
||||||
|
run: vendor/bin/phpstan analyse --ansi
|
||||||
|
|
||||||
|
- name: Unit Tests
|
||||||
|
run: bin/pest --colors=always --exclude-group=integration
|
||||||
|
|
||||||
|
- name: Integration Tests
|
||||||
|
run: bin/pest --colors=always --group=integration
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.idea/*
|
||||||
|
.idea/codeStyleSettings.xml
|
||||||
|
composer.lock
|
||||||
|
/vendor/
|
||||||
|
coverage.xml
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php_cs.cache
|
||||||
|
.temp/coverage.php
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
32
.php_cs
Normal file
32
.php_cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$finder = PhpCsFixer\Finder::create()
|
||||||
|
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
|
||||||
|
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
|
||||||
|
->in(__DIR__ . DIRECTORY_SEPARATOR . 'compiled')
|
||||||
|
->in(__DIR__ . DIRECTORY_SEPARATOR . 'scripts')
|
||||||
|
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
|
||||||
|
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
|
||||||
|
->append(['.php_cs']);
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'@Symfony' => true,
|
||||||
|
'phpdoc_no_empty_return' => false,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'yoda_style' => false,
|
||||||
|
'binary_operator_spaces' => [
|
||||||
|
'operators' => [
|
||||||
|
'=>' => 'align',
|
||||||
|
'=' => 'align',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'concat_space' => ['spacing' => 'one'],
|
||||||
|
'not_operator_with_space' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$rules['increment_style'] = ['style' => 'post'];
|
||||||
|
|
||||||
|
return PhpCsFixer\Config::create()
|
||||||
|
->setUsingCache(true)
|
||||||
|
->setRules($rules)
|
||||||
|
->setFinder($finder);
|
||||||
0
.temp/.gitkeep
Normal file
0
.temp/.gitkeep
Normal file
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Changelog
|
||||||
|
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]
|
||||||
|
|
||||||
|
## [v0.1]
|
||||||
|
### Added
|
||||||
|
- First version
|
||||||
52
CONTRIBUTING.md
Normal file
52
CONTRIBUTING.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
Contributions are welcome, and are accepted via pull requests.
|
||||||
|
Please review these guidelines before submitting any pull requests.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Fork the project
|
||||||
|
1. Create a new branch
|
||||||
|
1. Code, test, commit and push
|
||||||
|
1. Open a pull request detailing your changes. Make sure to follow the [template](.github/PULL_REQUEST_TEMPLATE.md)
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
* Please ensure the coding style running `composer lint`.
|
||||||
|
* Send a coherent commit history, making sure each individual commit in your pull request is meaningful.
|
||||||
|
* You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts.
|
||||||
|
* Please remember that we follow [SemVer](http://semver.org/).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Clone your fork, then install the dev dependencies:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
Lint your code:
|
||||||
|
```bash
|
||||||
|
composer lint
|
||||||
|
```
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Run all tests:
|
||||||
|
```bash
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
Check types:
|
||||||
|
```bash
|
||||||
|
composer test:types
|
||||||
|
```
|
||||||
|
|
||||||
|
Unit tests:
|
||||||
|
```bash
|
||||||
|
composer test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests:
|
||||||
|
```bash
|
||||||
|
composer test:integration
|
||||||
|
```
|
||||||
21
LICENSE.md
Executable file
21
LICENSE.md
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) Nuno Maduro <enunomaduro@gmail.com>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
17
README.md
Normal file
17
README.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="https://next.pestphp.com/assets/img/og.png" width="600" alt="PEST Preview">
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/pestphp/pest/actions"><img src="https://github.com/pest/pestphp/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://packagist.org/packages/pestphp/pest"><img src="https://poser.pugx.org/pestphp/pest/d/total.svg" alt="Total Downloads"></a>
|
||||||
|
<a href="https://packagist.org/packages/pestphp/pest"><img src="https://poser.pugx.org/pestphp/pest/v/stable.svg" alt="Latest Version"></a>
|
||||||
|
<a href="https://packagist.org/packages/pestphp/pest"><img src="https://poser.pugx.org/pestphp/pest/license.svg" alt="License"></a>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
------
|
||||||
|
**Pest** it's an elegant PHP Testing Framework with a focus on simplicity. It was carefully crafted to bring the joy of testing to PHP.
|
||||||
|
|
||||||
|
- Explore the docs: **[pestphp.com »](https://pestphp.com)**
|
||||||
|
- Join the Discord Server: **[discord.gg/4UMHUb5 »](https://discord.gg/4UMHUb5)**
|
||||||
|
|
||||||
|
Pest was created by **[Nuno Maduro](https://twitter.com/enunomaduro)** and is open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
||||||
31
bin/pest
Executable file
31
bin/pest
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
use Pest\Actions\ValidatesEnvironment;
|
||||||
|
use Pest\Console\Command;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
|
(static function () {
|
||||||
|
// Used when Pest is required using composer.
|
||||||
|
$vendorPath = realpath(__DIR__ . '/../../../../vendor/autoload.php');
|
||||||
|
|
||||||
|
// Used when Pest maintainers are running Pest tests.
|
||||||
|
$localPath = realpath(__DIR__ . '/../vendor/autoload.php');
|
||||||
|
|
||||||
|
if ($vendorPath) {
|
||||||
|
include_once $vendorPath;
|
||||||
|
} else {
|
||||||
|
include_once $localPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
(new \NunoMaduro\Collision\Provider)->register();
|
||||||
|
|
||||||
|
$rootPath = getcwd();
|
||||||
|
|
||||||
|
$testSuite = TestSuite::getInstance($rootPath);
|
||||||
|
|
||||||
|
ValidatesEnvironment::in($testSuite);
|
||||||
|
|
||||||
|
exit((new Command($testSuite, new ConsoleOutput()))->run($_SERVER['argv']));
|
||||||
|
})();
|
||||||
0
compiled/.gitkeep
Normal file
0
compiled/.gitkeep
Normal file
1926
compiled/globals.php
Normal file
1926
compiled/globals.php
Normal file
File diff suppressed because it is too large
Load Diff
82
composer.json
Normal file
82
composer.json
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"name": "pestphp/pest",
|
||||||
|
"description": "An elegant PHP Testing Framework.",
|
||||||
|
"keywords": [
|
||||||
|
"php",
|
||||||
|
"framework",
|
||||||
|
"pest",
|
||||||
|
"unit",
|
||||||
|
"test",
|
||||||
|
"testing"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nuno Maduro",
|
||||||
|
"email": "enunomaduro@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.3",
|
||||||
|
"nunomaduro/collision": "^5.0",
|
||||||
|
"phpunit/phpunit": "^9.1.4",
|
||||||
|
"sebastian/environment": "^5.1"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Pest\\": "src/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/globals.php",
|
||||||
|
"compiled/globals.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/PHPUnit/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ergebnis/phpstan-rules": "^0.14.4",
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.16.3",
|
||||||
|
"illuminate/console": "^7.10.3",
|
||||||
|
"illuminate/support": "^7.10.3",
|
||||||
|
"mockery/mockery": "^1.3.1",
|
||||||
|
"phpstan/phpstan": "^0.12.25",
|
||||||
|
"phpstan/phpstan-strict-rules": "^0.12.2",
|
||||||
|
"rector/rector": "^0.7.25",
|
||||||
|
"symfony/var-dumper": "^5.0.8",
|
||||||
|
"thecodingmachine/phpstan-strict-rules": "^0.12.0"
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true,
|
||||||
|
"preferred-install": "dist"
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/pest"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"compile": "@php ./scripts/compile.php",
|
||||||
|
"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",
|
||||||
|
"test:unit": "bin/pest --colors=always --exclude-group=integration",
|
||||||
|
"test:integration": "bin/pest --colors=always --group=integration",
|
||||||
|
"test:integration:snapshots": "REBUILD_SNAPSHOTS=true bin/pest --colors=always",
|
||||||
|
"test": [
|
||||||
|
"@test:lint",
|
||||||
|
"@test:types",
|
||||||
|
"@test:unit",
|
||||||
|
"@test:integration"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Pest\\Laravel\\PestServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
phpstan.neon
Normal file
22
phpstan.neon
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
includes:
|
||||||
|
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||||
|
- vendor/ergebnis/phpstan-rules/rules.neon
|
||||||
|
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: max
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
excludes_analyse:
|
||||||
|
- src/globals.php
|
||||||
|
|
||||||
|
checkMissingIterableValueType: true
|
||||||
|
checkGenericClassInNonGenericObjectType: false
|
||||||
|
reportUnmatchedIgnoredErrors: true
|
||||||
|
|
||||||
|
ignoreErrors:
|
||||||
|
- "#is not allowed to extend#"
|
||||||
|
- "#Language construct eval#"
|
||||||
|
- "# with null as default value#"
|
||||||
|
- "#Using \\$this in static method#"
|
||||||
|
- "#has parameter \\$closure with default value.#"
|
||||||
16
phpunit.xml
Normal file
16
phpunit.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="default">
|
||||||
|
<directory suffix=".php">./tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
||||||
16
rector.yaml
Normal file
16
rector.yaml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# rector.yaml
|
||||||
|
parameters:
|
||||||
|
sets:
|
||||||
|
- 'action-injection-to-constructor-injection'
|
||||||
|
- 'array-str-functions-to-static-call'
|
||||||
|
- 'celebrity'
|
||||||
|
- 'doctrine'
|
||||||
|
- 'phpstan'
|
||||||
|
- 'phpunit-code-quality'
|
||||||
|
- 'solid'
|
||||||
|
- 'early-return'
|
||||||
|
- 'doctrine-code-quality'
|
||||||
|
- 'code-quality'
|
||||||
|
- 'php71'
|
||||||
|
- 'php72'
|
||||||
|
- 'php73'
|
||||||
36
scripts/compile.php
Normal file
36
scripts/compile.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?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);
|
||||||
79
src/Actions/AddsCoverage.php
Normal file
79
src/Actions/AddsCoverage.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Actions;
|
||||||
|
|
||||||
|
use Pest\Console\Coverage;
|
||||||
|
use Pest\Support\Str;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Input\InputDefinition;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AddsCoverage
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const COVERAGE_OPTION = 'coverage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const MIN_OPTION = 'min';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the coverage related options.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const OPTIONS = [self::COVERAGE_OPTION, self::MIN_OPTION];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If any, adds the coverage params to the given original arguments.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $originals
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function from(TestSuite $testSuite, array $originals): array
|
||||||
|
{
|
||||||
|
$arguments = array_merge([''], array_values(array_filter($originals, function ($original): bool {
|
||||||
|
foreach (self::OPTIONS 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)) {
|
||||||
|
$testSuite->coverage = true;
|
||||||
|
$originals[] = '--coverage-php';
|
||||||
|
$originals[] = Coverage::getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input->getOption(self::MIN_OPTION) !== null) {
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
$testSuite->coverageMin = (float) $input->getOption(self::MIN_OPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $originals;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/Actions/AddsDefaults.php
Normal file
29
src/Actions/AddsDefaults.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Actions;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\Printer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AddsDefaults
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds default arguments to the given `arguments` array.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $arguments
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function to(array $arguments): array
|
||||||
|
{
|
||||||
|
if (!array_key_exists('printer', $arguments)) {
|
||||||
|
$arguments['printer'] = new Printer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Actions/AddsTests.php
Normal file
66
src/Actions/AddsTests.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Actions;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use PHPUnit\Framework\TestSuite;
|
||||||
|
use PHPUnit\Framework\WarningTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AddsTests
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Adds tests to the given test suite.
|
||||||
|
*
|
||||||
|
* @param TestSuite<\PHPUnit\Framework\TestCase> $testSuite
|
||||||
|
*/
|
||||||
|
public static function to(TestSuite $testSuite, \Pest\TestSuite $pestTestSuite): void
|
||||||
|
{
|
||||||
|
self::removeTestClosureWarnings($testSuite);
|
||||||
|
|
||||||
|
// @todo refactor this...
|
||||||
|
|
||||||
|
$testSuites = [];
|
||||||
|
$pestTestSuite->tests->build($pestTestSuite, function (TestCase $testCase) use (&$testSuites): void {
|
||||||
|
$testCaseClass = get_class($testCase);
|
||||||
|
if (!array_key_exists($testCaseClass, $testSuites)) {
|
||||||
|
$testSuites[$testCaseClass] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$testSuites[$testCaseClass][] = $testCase;
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($testSuites as $testCaseName => $testCases) {
|
||||||
|
$testTestSuite = new TestSuite($testCaseName);
|
||||||
|
$testTestSuite->setTests([]);
|
||||||
|
foreach ($testCases as $testCase) {
|
||||||
|
$testTestSuite->addTest($testCase, $testCase->getGroups());
|
||||||
|
}
|
||||||
|
$testSuite->addTestSuite($testTestSuite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TestSuite<\PHPUnit\Framework\TestCase> $testSuite
|
||||||
|
*/
|
||||||
|
private static function removeTestClosureWarnings(TestSuite $testSuite): void
|
||||||
|
{
|
||||||
|
$tests = $testSuite->tests();
|
||||||
|
|
||||||
|
foreach ($tests as $key => $test) {
|
||||||
|
if ($test instanceof TestSuite) {
|
||||||
|
self::removeTestClosureWarnings($test);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($test instanceof WarningTestCase) {
|
||||||
|
unset($tests[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$testSuite->setTests($tests);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/Actions/LoadStructure.php
Normal file
61
src/Actions/LoadStructure.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Actions;
|
||||||
|
|
||||||
|
use Pest\Support\Str;
|
||||||
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
|
use PHPUnit\Util\FileLoader;
|
||||||
|
use RecursiveDirectoryIterator;
|
||||||
|
use RecursiveIteratorIterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class LoadStructure
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Pest convention.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const STRUCTURE = [
|
||||||
|
'Datasets.php',
|
||||||
|
'Pest.php',
|
||||||
|
'Datasets',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the configuration in the given `configuration`.
|
||||||
|
*/
|
||||||
|
public static function in(string $rootPath): void
|
||||||
|
{
|
||||||
|
$testsPath = $rootPath . DIRECTORY_SEPARATOR . 'tests';
|
||||||
|
|
||||||
|
$load = function ($filename): bool {
|
||||||
|
return file_exists($filename) && (bool) FileLoader::checkAndLoad($filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (self::STRUCTURE as $filename) {
|
||||||
|
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
|
||||||
|
|
||||||
|
if (!file_exists($filename)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir($filename)) {
|
||||||
|
$directory = new RecursiveDirectoryIterator($filename);
|
||||||
|
$iterator = new RecursiveIteratorIterator($directory);
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
$filename = $file->__toString();
|
||||||
|
if (Str::endsWith($filename, '.php') && file_exists($filename)) {
|
||||||
|
require_once $filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$load($filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/Actions/ValidatesConfiguration.php
Normal file
41
src/Actions/ValidatesConfiguration.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Actions;
|
||||||
|
|
||||||
|
use Pest\Exceptions\AttributeNotSupportedYet;
|
||||||
|
use Pest\Exceptions\FileOrFolderNotFound;
|
||||||
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
|
use PHPUnit\TextUI\Configuration\Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ValidatesConfiguration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private const CONFIGURATION_KEY = 'configuration';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the configuration in the given `configuration`.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public static function in($arguments): void
|
||||||
|
{
|
||||||
|
if (!array_key_exists(self::CONFIGURATION_KEY, $arguments) || !file_exists($arguments[self::CONFIGURATION_KEY])) {
|
||||||
|
throw new FileOrFolderNotFound('phpunit.xml');
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuration = Registry::getInstance()
|
||||||
|
->get($arguments[self::CONFIGURATION_KEY])
|
||||||
|
->phpunit();
|
||||||
|
|
||||||
|
if ($configuration->processIsolation()) {
|
||||||
|
throw new AttributeNotSupportedYet('processIsolation', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/Actions/ValidatesEnvironment.php
Normal file
42
src/Actions/ValidatesEnvironment.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Actions;
|
||||||
|
|
||||||
|
use Pest\Exceptions\FileOrFolderNotFound;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ValidatesEnvironment
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The need files on the root path.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const NEEDED_FILES = [
|
||||||
|
'composer.json',
|
||||||
|
'tests',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the environment.
|
||||||
|
*/
|
||||||
|
public static function in(TestSuite $testSuite): void
|
||||||
|
{
|
||||||
|
$rootPath = $testSuite->rootPath;
|
||||||
|
|
||||||
|
$exists = function ($neededFile) use ($rootPath): bool {
|
||||||
|
return file_exists(sprintf('%s%s%s', $rootPath, DIRECTORY_SEPARATOR, $neededFile));
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (self::NEEDED_FILES as $neededFile) {
|
||||||
|
if (!$exists($neededFile)) {
|
||||||
|
throw new FileOrFolderNotFound($neededFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/Concerns/TestCase.php
Normal file
145
src/Concerns/TestCase.php
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Concerns;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Support\ExceptionTrace;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Util\Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets executed before the test.
|
||||||
|
*/
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the test case as string.
|
||||||
|
*/
|
||||||
|
public function toString(): string
|
||||||
|
{
|
||||||
|
return \sprintf(
|
||||||
|
'%s::%s',
|
||||||
|
self::$__filename,
|
||||||
|
$this->__description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the test.
|
||||||
|
*/
|
||||||
|
public function __test(): void
|
||||||
|
{
|
||||||
|
$this->__callClosure($this->__test, func_get_args());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __callClosure(Closure $closure, array $arguments): void
|
||||||
|
{
|
||||||
|
ExceptionTrace::ensure(function () use ($closure, $arguments) {
|
||||||
|
call_user_func_array(Closure::bind($closure, $this, get_class($this)), $arguments);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrintableTestCaseName(): string
|
||||||
|
{
|
||||||
|
return ltrim(self::class, 'P\\');
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/Console/Command.php
Normal file
143
src/Console/Command.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Console;
|
||||||
|
|
||||||
|
use Pest\Actions\AddsCoverage;
|
||||||
|
use Pest\Actions\AddsDefaults;
|
||||||
|
use Pest\Actions\AddsTests;
|
||||||
|
use Pest\Actions\LoadStructure;
|
||||||
|
use Pest\Actions\ValidatesConfiguration;
|
||||||
|
use Pest\Exceptions\CodeCoverageDriverNotAvailable;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\TestSuite as BaseTestSuite;
|
||||||
|
use PHPUnit\TextUI\Command as BaseCommand;
|
||||||
|
use PHPUnit\TextUI\TestRunner;
|
||||||
|
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Command extends BaseCommand
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the current testing suite.
|
||||||
|
*
|
||||||
|
* @var TestSuite
|
||||||
|
*/
|
||||||
|
private $testSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current console output.
|
||||||
|
*
|
||||||
|
* @var OutputInterface
|
||||||
|
*/
|
||||||
|
private $output;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the command class.
|
||||||
|
*/
|
||||||
|
public function __construct(TestSuite $testSuite, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$this->testSuite = $testSuite;
|
||||||
|
$this->output = $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @phpstan-ignore-next-line
|
||||||
|
*
|
||||||
|
* @param array<int, string> $argv
|
||||||
|
*/
|
||||||
|
protected function handleArguments(array $argv): void
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* First, let's handle pest is own `--coverage` param.
|
||||||
|
*/
|
||||||
|
$argv = AddsCoverage::from($this->testSuite, $argv);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Next, as usual, let's send the console arguments to PHPUnit.
|
||||||
|
*/
|
||||||
|
parent::handleArguments($argv);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Finally, let's validate the configuration. Making
|
||||||
|
* sure all options are yet supported by Pest.
|
||||||
|
*/
|
||||||
|
ValidatesConfiguration::in($this->arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new PHPUnit test runner.
|
||||||
|
*/
|
||||||
|
protected function createRunner(): TestRunner
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* First, let's add the defaults we use on `pest`. Those
|
||||||
|
* are the printer class, and others that may be appear.
|
||||||
|
*/
|
||||||
|
$this->arguments = AddsDefaults::to($this->arguments);
|
||||||
|
|
||||||
|
$testRunner = new TestRunner($this->arguments['loader']);
|
||||||
|
$testSuite = $this->arguments['test'];
|
||||||
|
|
||||||
|
if (is_string($testSuite)) {
|
||||||
|
if (\is_dir($testSuite)) {
|
||||||
|
/** @var string[] $files */
|
||||||
|
$files = (new FileIteratorFacade())->getFilesAsArray(
|
||||||
|
$testSuite,
|
||||||
|
$this->arguments['testSuffixes']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$files = [$testSuite];
|
||||||
|
}
|
||||||
|
|
||||||
|
$testSuite = new BaseTestSuite($testSuite);
|
||||||
|
|
||||||
|
$testSuite->addTestFiles($files);
|
||||||
|
|
||||||
|
$this->arguments['test'] = $testSuite;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadStructure::in($this->testSuite->rootPath);
|
||||||
|
AddsTests::to($testSuite, $this->testSuite);
|
||||||
|
|
||||||
|
return $testRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @phpstan-ignore-next-line
|
||||||
|
*
|
||||||
|
* @param array<int, string> $argv
|
||||||
|
*/
|
||||||
|
public function run(array $argv, bool $exit = true): int
|
||||||
|
{
|
||||||
|
$result = parent::run($argv, false);
|
||||||
|
|
||||||
|
if ($result === 0 && $this->testSuite->coverage) {
|
||||||
|
if (!Coverage::isAvailable()) {
|
||||||
|
throw new CodeCoverageDriverNotAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverage = Coverage::report($this->output);
|
||||||
|
|
||||||
|
$result = (int) ($coverage < $this->testSuite->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->testSuite->coverageMin, 1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/Console/Coverage.php
Normal file
167
src/Console/Coverage.php
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Console;
|
||||||
|
|
||||||
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
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 \SebastianBergmann\CodeCoverage\CodeCoverage $codeCoverage */
|
||||||
|
$codeCoverage = require $reportPath;
|
||||||
|
unlink($reportPath);
|
||||||
|
|
||||||
|
$totalWidth = (new Terminal())->getWidth();
|
||||||
|
|
||||||
|
$dottedLineLength = $totalWidth <= 70 ? $totalWidth : 70;
|
||||||
|
|
||||||
|
$totalCoverage = $codeCoverage->getReport()->getLineExecutedPercent();
|
||||||
|
|
||||||
|
$output->writeln(
|
||||||
|
sprintf(
|
||||||
|
' <fg=white;options=bold>Cov: </><fg=default>%s</>',
|
||||||
|
$totalCoverage
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$output->writeln('');
|
||||||
|
|
||||||
|
/** @var Directory<File|Directory> $report */
|
||||||
|
$report = $codeCoverage->getReport();
|
||||||
|
|
||||||
|
foreach ($report->getIterator() as $file) {
|
||||||
|
if (!$file instanceof File) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$dirname = dirname($file->getId());
|
||||||
|
$basename = basename($file->getId(), '.php');
|
||||||
|
|
||||||
|
$name = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
|
||||||
|
$dirname,
|
||||||
|
$basename,
|
||||||
|
]);
|
||||||
|
$rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
|
||||||
|
$dirname,
|
||||||
|
$basename,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$linesExecutedTakenSize = 0;
|
||||||
|
|
||||||
|
if ($file->getLineExecutedPercent() != '0.00%') {
|
||||||
|
$linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1;
|
||||||
|
$name .= sprintf(' <fg=red>%s</>', $uncoveredLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
$percentage = $file->getNumExecutableLines() === 0
|
||||||
|
? '100.0'
|
||||||
|
: number_format((float) $file->getLineExecutedPercent(), 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 (float) $totalCoverage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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] = sprintf('%s..%s', $from, $line);
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
$array[$lastKey] = sprintf('%s..%s', $array[$lastKey], $line);
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
};
|
||||||
|
|
||||||
|
$array = [];
|
||||||
|
foreach (array_filter($file->getCoverageData(), 'is_array') as $line => $tests) {
|
||||||
|
$array = $eachLine($array, $tests, $line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Contracts/HasPrintableTestCaseName.php
Normal file
21
src/Contracts/HasPrintableTestCaseName.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
if (interface_exists(\NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName::class)) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface HasPrintableTestCaseName extends \NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName
|
||||||
|
{
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface HasPrintableTestCaseName
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/Datasets.php
Normal file
97
src/Datasets.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Exceptions\DatasetAlreadyExist;
|
||||||
|
use Pest\Exceptions\DatasetDoesNotExist;
|
||||||
|
use SebastianBergmann\Exporter\Exporter;
|
||||||
|
use Traversable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Datasets
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the datasets.
|
||||||
|
*
|
||||||
|
* @var array<string, \Closure|iterable<int, mixed>>
|
||||||
|
*/
|
||||||
|
private static $datasets = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the given.
|
||||||
|
*
|
||||||
|
* @param Closure|iterable<int, mixed> $data
|
||||||
|
*/
|
||||||
|
public static function set(string $name, $data): void
|
||||||
|
{
|
||||||
|
if (array_key_exists($name, self::$datasets)) {
|
||||||
|
throw new DatasetAlreadyExist($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$datasets[$name] = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Closure|iterable<int, mixed>
|
||||||
|
*/
|
||||||
|
public static function get(string $name)
|
||||||
|
{
|
||||||
|
if (!array_key_exists($name, self::$datasets)) {
|
||||||
|
throw new DatasetDoesNotExist($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$datasets[$name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the current dataset to an array value.
|
||||||
|
*
|
||||||
|
* @param Traversable<int, mixed>|Closure|iterable<int, mixed>|string|null $data
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function resolve(string $description, $data): array
|
||||||
|
{
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
if (is_null($data) || empty($data)) {
|
||||||
|
return [$description => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($data)) {
|
||||||
|
$data = self::get($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($data)) {
|
||||||
|
$data = call_user_func($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data instanceof Traversable) {
|
||||||
|
$data = iterator_to_array($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$namedData = [];
|
||||||
|
foreach ($data as $values) {
|
||||||
|
$values = is_array($values) ? $values : [$values];
|
||||||
|
|
||||||
|
$name = $description . self::getDataSetDescription($values);
|
||||||
|
$namedData[$name] = $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $namedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, mixed> $data
|
||||||
|
*/
|
||||||
|
private static function getDataSetDescription(array $data): string
|
||||||
|
{
|
||||||
|
$exporter = new Exporter();
|
||||||
|
|
||||||
|
return \sprintf(' with (%s)', $exporter->shortenedRecursiveExport($data));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/AfterAllAlreadyExist.php
Normal file
24
src/Exceptions/AfterAllAlreadyExist.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 AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of after all already exist exception.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The afterAll already exist in the filename `%s`.', $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/AfterEachAlreadyExist.php
Normal file
24
src/Exceptions/AfterEachAlreadyExist.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 AfterEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of after each already exist exception.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The afterEach already exist in the filename `%s`.', $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/AttributeNotSupportedYet.php
Normal file
24
src/Exceptions/AttributeNotSupportedYet.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 AttributeNotSupportedYet extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of attribute not supported yet.
|
||||||
|
*/
|
||||||
|
public function __construct(string $attribute, string $value)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The PHPUnit attribute `%s` with value `%s` is not supported yet.', $attribute, $value));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/BeforeEachAlreadyExist.php
Normal file
24
src/Exceptions/BeforeEachAlreadyExist.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 BeforeEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of before each already exist exception.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The beforeEach already exist in the filename `%s`.', $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/CodeCoverageDriverNotAvailable.php
Normal file
24
src/Exceptions/CodeCoverageDriverNotAvailable.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 CodeCoverageDriverNotAvailable extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of test already exist.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('No code coverage driver is available');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/DatasetAlreadyExist.php
Normal file
24
src/Exceptions/DatasetAlreadyExist.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 DatasetAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of dataset already exist.
|
||||||
|
*/
|
||||||
|
public function __construct(string $name)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('A dataset with the name `%s` already exist.', $name));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/DatasetDoesNotExist.php
Normal file
24
src/Exceptions/DatasetDoesNotExist.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 DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of dataset does not exist.
|
||||||
|
*/
|
||||||
|
public function __construct(string $name)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf("A dataset with the name `%s` does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/FileOrFolderNotFound.php
Normal file
24
src/Exceptions/FileOrFolderNotFound.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 FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of file not found.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The file or folder with the name `%s` not found.', $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/InvalidConsoleArgument.php
Normal file
24
src/Exceptions/InvalidConsoleArgument.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 InvalidConsoleArgument extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of should not happen.
|
||||||
|
*/
|
||||||
|
public function __construct(string $message)
|
||||||
|
{
|
||||||
|
parent::__construct($message, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/InvalidPestCommand.php
Normal file
24
src/Exceptions/InvalidPestCommand.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 InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of invalid pest command exception.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('Please run `./vendor/bin/pest` instead of `/vendor/bin/phpunit`.');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/InvalidUsesPath.php
Normal file
24
src/Exceptions/InvalidUsesPath.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 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Exceptions/ShouldNotHappen.php
Normal file
40
src/Exceptions/ShouldNotHappen.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ShouldNotHappen extends RuntimeException
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of should not happen.
|
||||||
|
*/
|
||||||
|
public function __construct(Exception $exception)
|
||||||
|
{
|
||||||
|
$message = $exception->getMessage();
|
||||||
|
|
||||||
|
parent::__construct(sprintf(<<<EOF
|
||||||
|
|
||||||
|
This should not happen - please create an new issue here: https://github.com/pestphp/pest.
|
||||||
|
|
||||||
|
- Issue: %s
|
||||||
|
- PHP version: %s
|
||||||
|
- Operating system: %s
|
||||||
|
EOF
|
||||||
|
, $message, phpversion(), PHP_OS), 1, $exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of should not happen without a specific exception.
|
||||||
|
*/
|
||||||
|
public static function fromMessage(string $message): ShouldNotHappen
|
||||||
|
{
|
||||||
|
return new ShouldNotHappen(new Exception($message));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/TestAlreadyExist.php
Normal file
24
src/Exceptions/TestAlreadyExist.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 TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of test already exist.
|
||||||
|
*/
|
||||||
|
public function __construct(string $fileName, string $description)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('A test with the description `%s` already exist in the filename `%s`.', $description, $fileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/Exceptions/TestCaseAlreadyInUse.php
Normal file
25
src/Exceptions/TestCaseAlreadyInUse.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?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 TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of test case already in use.
|
||||||
|
*/
|
||||||
|
public function __construct(string $inUse, string $newOne, string $folder)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('Test case `%s` can not be used. The folder `%s` already uses the test case `%s`',
|
||||||
|
$newOne, $folder, $inUse));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Exceptions/TestCaseClassOrTraitNotFound.php
Normal file
24
src/Exceptions/TestCaseClassOrTraitNotFound.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 TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of after each already exist exception.
|
||||||
|
*/
|
||||||
|
public function __construct(string $testCaseClass)
|
||||||
|
{
|
||||||
|
parent::__construct(sprintf('The class `%s` was not found.', $testCaseClass));
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/Factories/TestCaseFactory.php
Normal file
191
src/Factories/TestCaseFactory.php
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Factories;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Concerns;
|
||||||
|
use Pest\Contracts\HasPrintableTestCaseName;
|
||||||
|
use Pest\Datasets;
|
||||||
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TestCaseFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the test filename.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks this test case as only.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $only = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the test description.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the test closure.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var Closure
|
||||||
|
*/
|
||||||
|
public $test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the dataset, if any.
|
||||||
|
*
|
||||||
|
* @var Closure|iterable<int, mixed>|string|null
|
||||||
|
*/
|
||||||
|
public $dataset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FQN of the test case class.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $class = TestCase::class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of FQN of the class traits.
|
||||||
|
*
|
||||||
|
* @var array <int, string>
|
||||||
|
*/
|
||||||
|
public $traits = [
|
||||||
|
Concerns\TestCase::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the higher order messages
|
||||||
|
* for the factory that are proxyble.
|
||||||
|
*
|
||||||
|
* @var HigherOrderMessageCollection
|
||||||
|
*/
|
||||||
|
public $factoryProxies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the higher order
|
||||||
|
* messages that are proxyble.
|
||||||
|
*
|
||||||
|
* @var HigherOrderMessageCollection
|
||||||
|
*/
|
||||||
|
public $proxies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the higher order
|
||||||
|
* messages that are chainable.
|
||||||
|
*
|
||||||
|
* @var HigherOrderMessageCollection
|
||||||
|
*/
|
||||||
|
public $chains;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new anonymous test case pending object.
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename, string $description, Closure $closure = null)
|
||||||
|
{
|
||||||
|
$this->filename = $filename;
|
||||||
|
$this->description = $description;
|
||||||
|
$this->test = $closure ?? NullClosure::create();
|
||||||
|
|
||||||
|
$this->factoryProxies = new HigherOrderMessageCollection();
|
||||||
|
$this->proxies = new HigherOrderMessageCollection();
|
||||||
|
$this->chains = new HigherOrderMessageCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the anonymous test case.
|
||||||
|
*
|
||||||
|
* @return array<int, TestCase>
|
||||||
|
*/
|
||||||
|
public function build(TestSuite $testSuite): array
|
||||||
|
{
|
||||||
|
$chains = $this->chains;
|
||||||
|
$proxies = $this->proxies;
|
||||||
|
$factoryTest = $this->test;
|
||||||
|
|
||||||
|
$test = function () use ($chains, $proxies, $factoryTest): void {
|
||||||
|
$proxies->proxy($this);
|
||||||
|
$chains->chain($this);
|
||||||
|
call_user_func(Closure::bind($factoryTest, $this, get_class($this)), ...func_get_args());
|
||||||
|
};
|
||||||
|
|
||||||
|
$className = $this->makeClassFromFilename($this->filename);
|
||||||
|
|
||||||
|
$createTest = function ($description, $data) use ($className, $test) {
|
||||||
|
$testCase = new $className($test, $description, $data);
|
||||||
|
$this->factoryProxies->proxy($testCase);
|
||||||
|
|
||||||
|
return $testCase;
|
||||||
|
};
|
||||||
|
|
||||||
|
$datasets = Datasets::resolve($this->description, $this->dataset);
|
||||||
|
|
||||||
|
return array_map($createTest, array_keys($datasets), $datasets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a fully qualified class name
|
||||||
|
* from the given filename.
|
||||||
|
*/
|
||||||
|
public function makeClassFromFilename(string $filename): string
|
||||||
|
{
|
||||||
|
$rootPath = TestSuite::getInstance()->rootPath;
|
||||||
|
$relativePath = str_replace($rootPath . DIRECTORY_SEPARATOR, '', $filename);
|
||||||
|
// Strip out any %-encoded octets.
|
||||||
|
$relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath);
|
||||||
|
|
||||||
|
// Limit to A-Z, a-z, 0-9, '_', '-'.
|
||||||
|
$relativePath = (string) preg_replace('/[^A-Za-z0-9.\/]/', '', $relativePath);
|
||||||
|
|
||||||
|
$classFQN = 'P\\' . basename(ucfirst(str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath)), '.php');
|
||||||
|
|
||||||
|
if (class_exists($classFQN)) {
|
||||||
|
return $classFQN;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
|
||||||
|
$traitsCode = sprintf('use %s;', implode(', ', array_map(function ($trait): string {
|
||||||
|
return sprintf('\%s', $trait);
|
||||||
|
}, $this->traits)));
|
||||||
|
|
||||||
|
$partsFQN = explode('\\', $classFQN);
|
||||||
|
$className = array_pop($partsFQN);
|
||||||
|
$namespace = implode('\\', $partsFQN);
|
||||||
|
$baseClass = sprintf('\%s', $this->class);
|
||||||
|
|
||||||
|
eval("
|
||||||
|
namespace $namespace;
|
||||||
|
|
||||||
|
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
||||||
|
$traitsCode
|
||||||
|
|
||||||
|
private static \$__filename = '$filename';
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
return $classFQN;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Laravel/Commands/PestDatasetCommand.php
Normal file
67
src/Laravel/Commands/PestDatasetCommand.php
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Laravel\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pest\Exceptions\InvalidConsoleArgument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class PestDatasetCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The console command name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'pest:dataset {name : The name of the dataset}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create a new dataset file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
/** @var string $name */
|
||||||
|
$name = $this->argument('name');
|
||||||
|
|
||||||
|
$relativePath = sprintf('tests/Datasets/%s.php', ucfirst($name));
|
||||||
|
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
$target = base_path($relativePath);
|
||||||
|
|
||||||
|
if (File::exists($target)) {
|
||||||
|
throw new InvalidConsoleArgument(sprintf('%s already exist', $target));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File::exists(dirname($relativePath))) {
|
||||||
|
File::makeDirectory(dirname($relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = File::get(implode(DIRECTORY_SEPARATOR, [
|
||||||
|
dirname(__DIR__, 3),
|
||||||
|
'stubs',
|
||||||
|
'Dataset.php',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$name = mb_strtolower($name);
|
||||||
|
$contents = str_replace('{dataset_name}', $name, $contents);
|
||||||
|
|
||||||
|
$element = Str::singular($name);
|
||||||
|
$contents = str_replace('{dataset_element}', $element, $contents);
|
||||||
|
File::put($target, str_replace('{dataset_name}', $name, $contents));
|
||||||
|
|
||||||
|
$this->output->success(sprintf('`%s` created successfully.', $relativePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Laravel/Commands/PestInstallCommand.php
Normal file
50
src/Laravel/Commands/PestInstallCommand.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Laravel\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Pest\Exceptions\InvalidConsoleArgument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class PestInstallCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The console command name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'pest:install';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Creates Pest resources in your current PHPUnit test suite';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
$target = base_path('tests/Pest.php');
|
||||||
|
|
||||||
|
if (File::exists($target)) {
|
||||||
|
throw new InvalidConsoleArgument(sprintf('%s already exist', $target));
|
||||||
|
}
|
||||||
|
|
||||||
|
File::copy(implode(DIRECTORY_SEPARATOR, [
|
||||||
|
dirname(__DIR__, 3),
|
||||||
|
'stubs',
|
||||||
|
'Pest.php',
|
||||||
|
]), $target);
|
||||||
|
|
||||||
|
$this->output->success('`tests/Pest.php` created successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/Laravel/Commands/PestTestCommand.php
Normal file
70
src/Laravel/Commands/PestTestCommand.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Laravel\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Pest\Exceptions\InvalidConsoleArgument;
|
||||||
|
use Pest\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class PestTestCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The console command name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'pest:test {name : The name of the file} {--unit : Create a unit test}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create a new test file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
/** @var string $name */
|
||||||
|
$name = $this->argument('name');
|
||||||
|
|
||||||
|
$type = ((bool) $this->option('unit')) ? 'Unit' : 'Feature';
|
||||||
|
|
||||||
|
$relativePath = sprintf('tests/%s/%s.php',
|
||||||
|
$type,
|
||||||
|
ucfirst($name)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
$target = base_path($relativePath);
|
||||||
|
|
||||||
|
if (!File::isDirectory(dirname($target))) {
|
||||||
|
File::makeDirectory(dirname($target), 0777, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File::exists($target)) {
|
||||||
|
throw new InvalidConsoleArgument(sprintf('%s already exist', $target));
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = File::get(implode(DIRECTORY_SEPARATOR, [
|
||||||
|
dirname(__DIR__, 3),
|
||||||
|
'stubs',
|
||||||
|
sprintf('%s.php', $type),
|
||||||
|
]));
|
||||||
|
|
||||||
|
$name = mb_strtolower($name);
|
||||||
|
$name = Str::endsWith($name, 'test') ? mb_substr($name, 0, -4) : $name;
|
||||||
|
|
||||||
|
File::put($target, str_replace('{name}', $name, $contents));
|
||||||
|
|
||||||
|
$this->output->success(sprintf('`%s` created successfully.', $relativePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Laravel/PestServiceProvider.php
Normal file
27
src/Laravel/PestServiceProvider.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Laravel;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Pest\Laravel\Commands\PestDatasetCommand;
|
||||||
|
use Pest\Laravel\Commands\PestInstallCommand;
|
||||||
|
use Pest\Laravel\Commands\PestTestCommand;
|
||||||
|
|
||||||
|
final class PestServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register artisan commands.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
$this->commands([
|
||||||
|
PestInstallCommand::class,
|
||||||
|
PestTestCommand::class,
|
||||||
|
PestDatasetCommand::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/PendingObjects/AfterEachCall.php
Normal file
86
src/PendingObjects/AfterEachCall.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\PendingObjects;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\ChainableClosure;
|
||||||
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AfterEachCall
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the test suite.
|
||||||
|
*
|
||||||
|
* @var TestSuite
|
||||||
|
*/
|
||||||
|
private $testSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the filename.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the before each closure.
|
||||||
|
*
|
||||||
|
* @var Closure
|
||||||
|
*/
|
||||||
|
private $closure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds calls that should be proxied.
|
||||||
|
*
|
||||||
|
* @var HigherOrderMessageCollection
|
||||||
|
*/
|
||||||
|
private $proxies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of before each call.
|
||||||
|
*/
|
||||||
|
public function __construct(TestSuite $testSuite, string $filename, Closure $closure = null)
|
||||||
|
{
|
||||||
|
$this->testSuite = $testSuite;
|
||||||
|
$this->filename = $filename;
|
||||||
|
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
||||||
|
|
||||||
|
$this->proxies = new HigherOrderMessageCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the creation of each call.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$proxies = $this->proxies;
|
||||||
|
|
||||||
|
$this->testSuite->afterEach->set(
|
||||||
|
$this->filename,
|
||||||
|
ChainableClosure::from(function () use ($proxies): void {
|
||||||
|
$proxies->chain($this);
|
||||||
|
}, $this->closure)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the calls to be used on the target.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function __call(string $name, array $arguments): self
|
||||||
|
{
|
||||||
|
$this->proxies
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/PendingObjects/BeforeEachCall.php
Normal file
86
src/PendingObjects/BeforeEachCall.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\PendingObjects;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\ChainableClosure;
|
||||||
|
use Pest\Support\HigherOrderMessageCollection;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BeforeEachCall
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the test suite.
|
||||||
|
*
|
||||||
|
* @var TestSuite
|
||||||
|
*/
|
||||||
|
private $testSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the filename.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the before each closure.
|
||||||
|
*
|
||||||
|
* @var Closure
|
||||||
|
*/
|
||||||
|
private $closure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds calls that should be proxied.
|
||||||
|
*
|
||||||
|
* @var HigherOrderMessageCollection
|
||||||
|
*/
|
||||||
|
private $proxies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of before each call.
|
||||||
|
*/
|
||||||
|
public function __construct(TestSuite $testSuite, string $filename, Closure $closure = null)
|
||||||
|
{
|
||||||
|
$this->testSuite = $testSuite;
|
||||||
|
$this->filename = $filename;
|
||||||
|
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
||||||
|
|
||||||
|
$this->proxies = new HigherOrderMessageCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the creation of each call.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$proxies = $this->proxies;
|
||||||
|
|
||||||
|
$this->testSuite->beforeEach->set(
|
||||||
|
$this->filename,
|
||||||
|
ChainableClosure::from(function () use ($proxies): void {
|
||||||
|
$proxies->chain($this);
|
||||||
|
}, $this->closure)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the calls to be used on the target.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function __call(string $name, array $arguments): self
|
||||||
|
{
|
||||||
|
$this->proxies
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/PendingObjects/TestCall.php
Normal file
133
src/PendingObjects/TestCall.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\PendingObjects;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Factories\TestCaseFactory;
|
||||||
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TestCall
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the test case factory.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var TestCaseFactory
|
||||||
|
*/
|
||||||
|
private $testCaseFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of a pending test call.
|
||||||
|
*/
|
||||||
|
public function __construct(TestSuite $testSuite, string $filename, string $description, Closure $closure = null)
|
||||||
|
{
|
||||||
|
$this->testCaseFactory = new TestCaseFactory($filename, $description, $closure);
|
||||||
|
|
||||||
|
$testSuite->tests->set($this->testCaseFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the test throws the given `$exceptionClass` when called.
|
||||||
|
*/
|
||||||
|
public function throws(string $exceptionClass, string $exceptionMessage = null): TestCall
|
||||||
|
{
|
||||||
|
$this->testCaseFactory
|
||||||
|
->proxies
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exceptionClass]);
|
||||||
|
|
||||||
|
if (is_string($exceptionMessage)) {
|
||||||
|
$this->testCaseFactory
|
||||||
|
->proxies
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the current test multiple times with
|
||||||
|
* each item of the given `iterable`.
|
||||||
|
*
|
||||||
|
* @param \Closure|iterable<int, mixed>|string $data
|
||||||
|
*/
|
||||||
|
public function with($data): TestCall
|
||||||
|
{
|
||||||
|
$this->testCaseFactory->dataset = $data;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the test suite only this test case.
|
||||||
|
*/
|
||||||
|
public function only(): TestCall
|
||||||
|
{
|
||||||
|
$this->testCaseFactory->only = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test groups(s).
|
||||||
|
*/
|
||||||
|
public function group(string ...$groups): TestCall
|
||||||
|
{
|
||||||
|
$this->testCaseFactory
|
||||||
|
->factoryProxies
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), 'addGroups', [$groups]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the current test.
|
||||||
|
*
|
||||||
|
* @param Closure|bool|string $conditionOrMessage
|
||||||
|
*/
|
||||||
|
public function skip($conditionOrMessage = true, string $message = ''): TestCall
|
||||||
|
{
|
||||||
|
$condition = is_string($conditionOrMessage)
|
||||||
|
? NullClosure::create()
|
||||||
|
: $conditionOrMessage;
|
||||||
|
|
||||||
|
$condition = is_callable($condition)
|
||||||
|
? $condition
|
||||||
|
: function () use ($condition) { /* @phpstan-ignore-line */
|
||||||
|
return $condition;
|
||||||
|
};
|
||||||
|
|
||||||
|
$message = is_string($conditionOrMessage)
|
||||||
|
? $conditionOrMessage
|
||||||
|
: $message;
|
||||||
|
|
||||||
|
if ($condition() !== false) {
|
||||||
|
$this->testCaseFactory
|
||||||
|
->chains
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the calls to be used on the target.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function __call(string $name, array $arguments): self
|
||||||
|
{
|
||||||
|
$this->testCaseFactory
|
||||||
|
->chains
|
||||||
|
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/PendingObjects/UsesCall.php
Normal file
97
src/PendingObjects/UsesCall.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\PendingObjects;
|
||||||
|
|
||||||
|
use Pest\Exceptions\InvalidUsesPath;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class UsesCall
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the class and traits.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private $classAndTraits;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the base dirname here the uses call was performed.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the targets of the uses.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private $targets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the groups of the uses.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private $groups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of a pending test uses.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $classAndTraits
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename, array $classAndTraits)
|
||||||
|
{
|
||||||
|
$this->classAndTraits = $classAndTraits;
|
||||||
|
$this->filename = $filename;
|
||||||
|
$this->targets = [$filename];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The directories or file where the
|
||||||
|
* class or trais should be used.
|
||||||
|
*/
|
||||||
|
public function in(string ...$targets): void
|
||||||
|
{
|
||||||
|
$targets = array_map(function ($path): string {
|
||||||
|
return $path[0] === DIRECTORY_SEPARATOR
|
||||||
|
? $path
|
||||||
|
: implode(DIRECTORY_SEPARATOR, [
|
||||||
|
dirname($this->filename),
|
||||||
|
$path,
|
||||||
|
]);
|
||||||
|
}, $targets);
|
||||||
|
|
||||||
|
$this->targets = array_map(function ($target): string {
|
||||||
|
$realTarget = realpath($target);
|
||||||
|
if ($realTarget === false) {
|
||||||
|
throw new InvalidUsesPath($target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $realTarget;
|
||||||
|
}, $targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the test group(s).
|
||||||
|
*/
|
||||||
|
public function group(string ...$groups): UsesCall
|
||||||
|
{
|
||||||
|
$this->groups = $groups;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the creation of uses.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
TestSuite::getInstance()->tests->use($this->classAndTraits, $this->groups, $this->targets);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Repositories/AfterAllRepository.php
Normal file
53
src/Repositories/AfterAllRepository.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Exceptions\AfterAllAlreadyExist;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\Support\Reflection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AfterAllRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, Closure>
|
||||||
|
*/
|
||||||
|
private $state = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given closure for each after all.
|
||||||
|
*/
|
||||||
|
public function each(callable $each): void
|
||||||
|
{
|
||||||
|
foreach ($this->state as $filename => $closure) {
|
||||||
|
$each($filename, $closure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a after all closure.
|
||||||
|
*/
|
||||||
|
public function set(Closure $closure): void
|
||||||
|
{
|
||||||
|
$filename = Reflection::getFileNameFromClosure($closure);
|
||||||
|
|
||||||
|
if (array_key_exists($filename, $this->state)) {
|
||||||
|
throw new AfterAllAlreadyExist($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state[$filename] = $closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a after all closure by the given filename.
|
||||||
|
*/
|
||||||
|
public function get(string $filename): Closure
|
||||||
|
{
|
||||||
|
return $this->state[$filename] ?? NullClosure::create();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Repositories/AfterEachRepository.php
Normal file
48
src/Repositories/AfterEachRepository.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Mockery;
|
||||||
|
use Pest\Exceptions\AfterEachAlreadyExist;
|
||||||
|
use Pest\Support\ChainableClosure;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class AfterEachRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, Closure>
|
||||||
|
*/
|
||||||
|
private $state = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a after each closure.
|
||||||
|
*/
|
||||||
|
public function set(string $filename, Closure $closure): void
|
||||||
|
{
|
||||||
|
if (array_key_exists($filename, $this->state)) {
|
||||||
|
throw new AfterEachAlreadyExist($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state[$filename] = $closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a after each closure by the given filename.
|
||||||
|
*/
|
||||||
|
public function get(string $filename): Closure
|
||||||
|
{
|
||||||
|
$afterEach = $this->state[$filename] ?? NullClosure::create();
|
||||||
|
|
||||||
|
return ChainableClosure::from(function (): void {
|
||||||
|
if (class_exists(Mockery::class)) {
|
||||||
|
Mockery::close();
|
||||||
|
}
|
||||||
|
}, $afterEach);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/Repositories/BeforeAllRepository.php
Normal file
55
src/Repositories/BeforeAllRepository.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Exceptions\BeforeEachAlreadyExist;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
use Pest\Support\Reflection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BeforeAllRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, Closure>
|
||||||
|
*/
|
||||||
|
private $state = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs one before all closure, and unsets it from the repository.
|
||||||
|
*/
|
||||||
|
public function pop(string $filename): Closure
|
||||||
|
{
|
||||||
|
$closure = $this->get($filename);
|
||||||
|
|
||||||
|
unset($this->state[$filename]);
|
||||||
|
|
||||||
|
return $closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a before all closure.
|
||||||
|
*/
|
||||||
|
public function set(Closure $closure): void
|
||||||
|
{
|
||||||
|
$filename = Reflection::getFileNameFromClosure($closure);
|
||||||
|
|
||||||
|
if (array_key_exists($filename, $this->state)) {
|
||||||
|
throw new BeforeEachAlreadyExist($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state[$filename] = $closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a before all closure by the given filename.
|
||||||
|
*/
|
||||||
|
public function get(string $filename): Closure
|
||||||
|
{
|
||||||
|
return $this->state[$filename] ?? NullClosure::create();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Repositories/BeforeEachRepository.php
Normal file
40
src/Repositories/BeforeEachRepository.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Exceptions\BeforeEachAlreadyExist;
|
||||||
|
use Pest\Support\NullClosure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BeforeEachRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, Closure>
|
||||||
|
*/
|
||||||
|
private $state = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a before each closure.
|
||||||
|
*/
|
||||||
|
public function set(string $filename, Closure $closure): void
|
||||||
|
{
|
||||||
|
if (array_key_exists($filename, $this->state)) {
|
||||||
|
throw new BeforeEachAlreadyExist($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state[$filename] = $closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a before each closure by the given filename.
|
||||||
|
*/
|
||||||
|
public function get(string $filename): Closure
|
||||||
|
{
|
||||||
|
return $this->state[$filename] ?? NullClosure::create();
|
||||||
|
}
|
||||||
|
}
|
||||||
122
src/Repositories/TestRepository.php
Normal file
122
src/Repositories/TestRepository.php
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Repositories;
|
||||||
|
|
||||||
|
use Pest\Exceptions\TestAlreadyExist;
|
||||||
|
use Pest\Exceptions\TestCaseAlreadyInUse;
|
||||||
|
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
|
||||||
|
use Pest\Factories\TestCaseFactory;
|
||||||
|
use Pest\Support\Str;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TestRepository
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, TestCaseFactory>
|
||||||
|
*/
|
||||||
|
private $state = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<int, array<int, string>>>
|
||||||
|
*/
|
||||||
|
private $uses = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of test cases.
|
||||||
|
*/
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the given callable foreach test case.
|
||||||
|
*/
|
||||||
|
public function build(TestSuite $testSuite, callable $each): void
|
||||||
|
{
|
||||||
|
$startsWith = function (string $target, string $directory): bool {
|
||||||
|
return Str::startsWith($target, $directory . DIRECTORY_SEPARATOR);
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($this->uses as $path => $uses) {
|
||||||
|
[$classOrTraits, $groups] = $uses;
|
||||||
|
$setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith): void {
|
||||||
|
[$filename] = explode('@', $key);
|
||||||
|
|
||||||
|
if ((!is_dir($path) && $filename === $path) || (is_dir($path) && $startsWith($filename, $path))) {
|
||||||
|
foreach ($classOrTraits as $class) {
|
||||||
|
if (class_exists($class)) {
|
||||||
|
if ($testCase->class !== \PHPUnit\Framework\TestCase::class) {
|
||||||
|
throw new TestCaseAlreadyInUse($testCase->class, $class, $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$testCase->class = $class;
|
||||||
|
} elseif (trait_exists($class)) {
|
||||||
|
$testCase->traits[] = $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$testCase
|
||||||
|
->factoryProxies
|
||||||
|
// Consider set the real line here.
|
||||||
|
->add($filename, 0, 'addGroups', [$groups]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($this->state as $key => $test) {
|
||||||
|
$setClassName($test, $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$onlyState = array_filter($this->state, function ($testFactory): bool {
|
||||||
|
return $testFactory->only;
|
||||||
|
});
|
||||||
|
|
||||||
|
$state = count($onlyState) > 0 ? $onlyState : $this->state;
|
||||||
|
|
||||||
|
foreach ($state as $testFactory) {
|
||||||
|
/* @var TestCaseFactory $testFactory */
|
||||||
|
$tests = $testFactory->build($testSuite);
|
||||||
|
foreach ($tests as $test) {
|
||||||
|
$each($test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the given `$testCaseClass` on the given `$paths`.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $classOrTraits
|
||||||
|
* @param array<int, string> $groups
|
||||||
|
* @param array<int, string> $paths
|
||||||
|
*/
|
||||||
|
public function use(array $classOrTraits, array $groups, array $paths): void
|
||||||
|
{
|
||||||
|
foreach ($classOrTraits as $classOrTrait) {
|
||||||
|
if (!class_exists($classOrTrait) && !trait_exists($classOrTrait)) {
|
||||||
|
throw new TestCaseClassOrTraitNotFound($classOrTrait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
$this->uses[$path] = [$classOrTraits, $groups];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a test case by the given filename and description.
|
||||||
|
*/
|
||||||
|
public function set(TestCaseFactory $test): void
|
||||||
|
{
|
||||||
|
if (array_key_exists(sprintf('%s@%s', $test->filename, $test->description), $this->state)) {
|
||||||
|
throw new TestAlreadyExist($test->filename, $test->description);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->state[sprintf('%s@%s', $test->filename, $test->description)] = $test;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Support/Backtrace.php
Normal file
35
src/Support/Backtrace.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Backtrace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the filename that called the current function/method.
|
||||||
|
*/
|
||||||
|
public static function file(): string
|
||||||
|
{
|
||||||
|
return debug_backtrace()[1]['file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the dirname that called the current function/method.
|
||||||
|
*/
|
||||||
|
public static function dirname(): string
|
||||||
|
{
|
||||||
|
return dirname(debug_backtrace()[1]['file']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the line that called the current function/method.
|
||||||
|
*/
|
||||||
|
public static function line(): int
|
||||||
|
{
|
||||||
|
return debug_backtrace()[1]['line'];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Support/ChainableClosure.php
Normal file
24
src/Support/ChainableClosure.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ChainableClosure
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calls the given `$closure` and chains the the `$next` closure.
|
||||||
|
*/
|
||||||
|
public static function from(Closure $closure, Closure $next): Closure
|
||||||
|
{
|
||||||
|
return function () use ($closure, $next): void {
|
||||||
|
call_user_func_array(Closure::bind($closure, $this, get_class($this)), func_get_args());
|
||||||
|
call_user_func_array(Closure::bind($next, $this, get_class($this)), func_get_args());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Support/ExceptionTrace.php
Normal file
35
src/Support/ExceptionTrace.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class ExceptionTrace
|
||||||
|
{
|
||||||
|
private const UNDEFINED_METHOD = 'Call to undefined method P\\';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the given closure reports
|
||||||
|
* the good execution context.
|
||||||
|
*/
|
||||||
|
public static function ensure(Closure $closure): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$closure();
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
if (Str::startsWith($message = $throwable->getMessage(), self::UNDEFINED_METHOD)) {
|
||||||
|
$message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message);
|
||||||
|
|
||||||
|
Reflection::setPropertyValue($throwable, 'message', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/Support/HigherOrderMessage.php
Normal file
60
src/Support/HigherOrderMessage.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class HigherOrderMessage
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The filename where the function was originally called.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The line where the function was originally called.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $line;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The method name.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $methodName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The arguments.
|
||||||
|
*
|
||||||
|
* @var array<int, mixed>
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
public $arguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new higher order message.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function __construct(string $filename, int $line, string $methodName, array $arguments)
|
||||||
|
{
|
||||||
|
$this->filename = $filename;
|
||||||
|
$this->line = $line;
|
||||||
|
$this->methodName = $methodName;
|
||||||
|
$this->arguments = $arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/Support/HigherOrderMessageCollection.php
Normal file
74
src/Support/HigherOrderMessageCollection.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use ReflectionClass;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class HigherOrderMessageCollection
|
||||||
|
{
|
||||||
|
public const UNDEFINED_METHOD = 'Method %s does not exist';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int, HigherOrderMessage>
|
||||||
|
*/
|
||||||
|
private $messages = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new higher order message to the collection.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $arguments
|
||||||
|
*/
|
||||||
|
public function add(string $filename, int $line, string $methodName, array $arguments): void
|
||||||
|
{
|
||||||
|
$this->messages[] = new HigherOrderMessage($filename, $line, $methodName, $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy all the messages starting from the target.
|
||||||
|
*/
|
||||||
|
public function chain(object $target): void
|
||||||
|
{
|
||||||
|
foreach ($this->messages as $message) {
|
||||||
|
$target = $this->attempt($target, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy all the messages to the target.
|
||||||
|
*/
|
||||||
|
public function proxy(object $target): void
|
||||||
|
{
|
||||||
|
foreach ($this->messages as $message) {
|
||||||
|
$this->attempt($target, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-throws the given `$throwable` with the good line and filename.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function attempt(object $target, HigherOrderMessage $message)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Reflection::call($target, $message->methodName, $message->arguments);
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
Reflection::setPropertyValue($throwable, 'file', $message->filename);
|
||||||
|
Reflection::setPropertyValue($throwable, 'line', $message->line);
|
||||||
|
|
||||||
|
if ($throwable->getMessage() === sprintf(self::UNDEFINED_METHOD, $message->methodName)) {
|
||||||
|
/** @var \ReflectionClass $reflection */
|
||||||
|
$reflection = (new ReflectionClass($target))->getParentClass();
|
||||||
|
Reflection::setPropertyValue($throwable, 'message', sprintf('Call to undefined method %s::%s()', $reflection->getName(), $message->methodName));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/Support/NullClosure.php
Normal file
22
src/Support/NullClosure.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class NullClosure
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a nullable closure.
|
||||||
|
*/
|
||||||
|
public static function create(): Closure
|
||||||
|
{
|
||||||
|
return Closure::fromCallable(function (): void {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/Support/Reflection.php
Normal file
112
src/Support/Reflection.php
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
use ReflectionFunction;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Reflection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calls the given method with args on the given object.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $args
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function call(object $object, string $method, array $args = [])
|
||||||
|
{
|
||||||
|
$reflectionClass = new ReflectionClass($object);
|
||||||
|
|
||||||
|
$reflectionMethod = $reflectionClass->getMethod($method);
|
||||||
|
|
||||||
|
$reflectionMethod->setAccessible(true);
|
||||||
|
|
||||||
|
return $reflectionMethod->invoke($object, ...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infers the file name from the given closure.
|
||||||
|
*/
|
||||||
|
public static function getFileNameFromClosure(Closure $closure): string
|
||||||
|
{
|
||||||
|
$reflectionClosure = new ReflectionFunction($closure);
|
||||||
|
|
||||||
|
return (string) $reflectionClosure->getFileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the property value from of the given object.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public static function getPropertyValue(object $object, string $property)
|
||||||
|
{
|
||||||
|
$reflectionClass = new ReflectionClass($object);
|
||||||
|
|
||||||
|
$reflectionProperty = null;
|
||||||
|
|
||||||
|
while ($reflectionProperty === null) {
|
||||||
|
try {
|
||||||
|
/* @var ReflectionProperty $reflectionProperty */
|
||||||
|
$reflectionProperty = $reflectionClass->getProperty($property);
|
||||||
|
} catch (ReflectionException $reflectionException) {
|
||||||
|
$reflectionClass = $reflectionClass->getParentClass();
|
||||||
|
|
||||||
|
if (!$reflectionClass instanceof ReflectionClass) {
|
||||||
|
throw new ShouldNotHappen($reflectionException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reflectionProperty === null) {
|
||||||
|
throw ShouldNotHappen::fromMessage('Reflection property not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
|
||||||
|
return $reflectionProperty->getValue($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the property value of the given object.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public static function setPropertyValue(object $object, string $property, $value): void
|
||||||
|
{
|
||||||
|
/** @var ReflectionClass $reflectionClass */
|
||||||
|
$reflectionClass = new ReflectionClass($object);
|
||||||
|
|
||||||
|
$reflectionProperty = null;
|
||||||
|
|
||||||
|
while ($reflectionProperty === null) {
|
||||||
|
try {
|
||||||
|
/* @var ReflectionProperty $reflectionProperty */
|
||||||
|
$reflectionProperty = $reflectionClass->getProperty($property);
|
||||||
|
} catch (ReflectionException $reflectionException) {
|
||||||
|
$reflectionClass = $reflectionClass->getParentClass();
|
||||||
|
|
||||||
|
if (!$reflectionClass instanceof ReflectionClass) {
|
||||||
|
throw new ShouldNotHappen($reflectionException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reflectionProperty === null) {
|
||||||
|
throw ShouldNotHappen::fromMessage('Reflection property not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
$reflectionProperty->setValue($object, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/Support/Str.php
Normal file
32
src/Support/Str.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Str
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks if the given `$target` starts with the given `$search`.
|
||||||
|
*/
|
||||||
|
public static function startsWith(string $target, string $search): bool
|
||||||
|
{
|
||||||
|
return substr($target, 0, strlen($search)) === $search;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given `$target` ends with the given `$search`.
|
||||||
|
*/
|
||||||
|
public static function endsWith(string $target, string $search): bool
|
||||||
|
{
|
||||||
|
$length = strlen($search);
|
||||||
|
if ($length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($target, -$length) === $search;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/TestSuite.php
Normal file
111
src/TestSuite.php
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest;
|
||||||
|
|
||||||
|
use Pest\Exceptions\InvalidPestCommand;
|
||||||
|
use Pest\Repositories\AfterAllRepository;
|
||||||
|
use Pest\Repositories\AfterEachRepository;
|
||||||
|
use Pest\Repositories\BeforeAllRepository;
|
||||||
|
use Pest\Repositories\BeforeEachRepository;
|
||||||
|
use Pest\Repositories\TestRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class TestSuite
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Holds the tests repository.
|
||||||
|
*
|
||||||
|
* @var TestRepository
|
||||||
|
*/
|
||||||
|
public $tests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether should show the coverage or not.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $coverage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum coverage.
|
||||||
|
*
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
public $coverageMin = 0.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the before each repository.
|
||||||
|
*
|
||||||
|
* @var BeforeEachRepository
|
||||||
|
*/
|
||||||
|
public $beforeEach;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the before all repository.
|
||||||
|
*
|
||||||
|
* @var BeforeAllRepository
|
||||||
|
*/
|
||||||
|
public $beforeAll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the after each repository.
|
||||||
|
*
|
||||||
|
* @var AfterEachRepository
|
||||||
|
*/
|
||||||
|
public $afterEach;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the after all repository.
|
||||||
|
*
|
||||||
|
* @var AfterAllRepository
|
||||||
|
*/
|
||||||
|
public $afterAll;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the root path.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $rootPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds an instance of the test suite.
|
||||||
|
*
|
||||||
|
* @var TestSuite
|
||||||
|
*/
|
||||||
|
private static $instance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the test suite.
|
||||||
|
*/
|
||||||
|
public function __construct(string $rootPath)
|
||||||
|
{
|
||||||
|
$this->beforeAll = new BeforeAllRepository();
|
||||||
|
$this->beforeEach = new BeforeEachRepository();
|
||||||
|
$this->tests = new TestRepository();
|
||||||
|
$this->afterEach = new AfterEachRepository();
|
||||||
|
$this->afterAll = new AfterAllRepository();
|
||||||
|
|
||||||
|
$this->rootPath = $rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current instance of the test suite.
|
||||||
|
*/
|
||||||
|
public static function getInstance(string $rootPath = null): TestSuite
|
||||||
|
{
|
||||||
|
if (is_string($rootPath)) {
|
||||||
|
return self::$instance ?? self::$instance = new TestSuite($rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$instance === null) {
|
||||||
|
throw new InvalidPestCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/globals.php
Normal file
101
src/globals.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Pest\Datasets;
|
||||||
|
use Pest\PendingObjects\AfterEachCall;
|
||||||
|
use Pest\PendingObjects\BeforeEachCall;
|
||||||
|
use Pest\PendingObjects\TestCall;
|
||||||
|
use Pest\PendingObjects\UsesCall;
|
||||||
|
use Pest\Support\Backtrace;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given closure after 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 adds the 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, Closure $closure = null): TestCall
|
||||||
|
{
|
||||||
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
$filename = Backtrace::file();
|
||||||
|
|
||||||
|
return new TestCall(TestSuite::getInstance(), $filename, sprintf('it %s', $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);
|
||||||
|
}
|
||||||
5
stubs/Dataset.php
Normal file
5
stubs/Dataset.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
dataset('{dataset_name}', function () {
|
||||||
|
return ['{dataset_element} A', '{dataset_element} B'];
|
||||||
|
});
|
||||||
7
stubs/Feature.php
Normal file
7
stubs/Feature.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('has {name} page', function () {
|
||||||
|
$response = $this->get('/{name}');
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
});
|
||||||
3
stubs/Pest.php
Normal file
3
stubs/Pest.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
uses(Tests\TestCase::class)->in('Feature');
|
||||||
5
stubs/Unit.php
Normal file
5
stubs/Unit.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('{name}', function () {
|
||||||
|
assertTrue(true);
|
||||||
|
});
|
||||||
21
stubs/phpunit.xml
Normal file
21
stubs/phpunit.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory suffix="Test.php">./tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory suffix="Test.php">./tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<filter>
|
||||||
|
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||||
|
<directory suffix=".php">./app</directory>
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
||||||
130
tests/.snapshots/success.txt
Normal file
130
tests/.snapshots/success.txt
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
PASS Tests\CustomTestCase\PhpunitTest
|
||||||
|
✓ that gets executed
|
||||||
|
|
||||||
|
PASS Tests\Features\AfterAll
|
||||||
|
✓ deletes file after all
|
||||||
|
|
||||||
|
PASS Tests\Features\AfterEach
|
||||||
|
✓ it does not get executed before the test
|
||||||
|
✓ it gets executed after the test
|
||||||
|
|
||||||
|
PASS Tests\Features\BeforeAll
|
||||||
|
✓ it gets executed before tests
|
||||||
|
✓ it do not get executed before each test
|
||||||
|
|
||||||
|
PASS Tests\Features\BeforeEach
|
||||||
|
✓ it gets executed before each test
|
||||||
|
✓ it gets executed before each test once again
|
||||||
|
|
||||||
|
PASS Tests\Features\Datasets
|
||||||
|
✓ it throws exception if dataset does not exist
|
||||||
|
✓ it throws exception if dataset already exist
|
||||||
|
✓ it sets closures
|
||||||
|
✓ it sets arrays
|
||||||
|
✓ it gets bound to test case object with ('a')
|
||||||
|
✓ it gets bound to test case object with ('b')
|
||||||
|
✓ it truncates the description with (' fooo fooo fooo fooo fooo fooo fooo f...oo fooo')
|
||||||
|
✓ lazy datasets with (1)
|
||||||
|
✓ lazy datasets with (2)
|
||||||
|
✓ lazy datasets did the job right
|
||||||
|
✓ eager datasets with (1)
|
||||||
|
✓ eager datasets with (2)
|
||||||
|
✓ eager datasets did the job right
|
||||||
|
✓ lazy registered datasets with (1)
|
||||||
|
✓ lazy registered datasets with (2)
|
||||||
|
✓ lazy registered datasets did the job right
|
||||||
|
✓ eager registered datasets with (1)
|
||||||
|
✓ eager registered datasets with (2)
|
||||||
|
✓ eager registered datasets did the job right
|
||||||
|
✓ eager wrapped registered datasets with (1)
|
||||||
|
✓ eager wrapped registered datasets with (2)
|
||||||
|
✓ eager registered wrapped datasets did the job right
|
||||||
|
✓ lazy named datasets with ( bar object (...))
|
||||||
|
|
||||||
|
PASS Tests\Features\Exceptions
|
||||||
|
✓ it gives access the the underlying expect exception
|
||||||
|
✓ it catch exceptions
|
||||||
|
✓ it catch exceptions and messages
|
||||||
|
|
||||||
|
PASS Tests\Features\HigherOrderMessages
|
||||||
|
✓ it proxies calls to object
|
||||||
|
|
||||||
|
PASS Tests\Features\It
|
||||||
|
✓ it is a test
|
||||||
|
✓ it is a higher order message test
|
||||||
|
|
||||||
|
PASS Tests\Features\Mocks
|
||||||
|
✓ it has bar
|
||||||
|
|
||||||
|
WARN Tests\Features\Skip
|
||||||
|
✓ it do not skips
|
||||||
|
s it skips with truthy
|
||||||
|
s it skips with truthy condition by default
|
||||||
|
s it skips with message → skipped because bar
|
||||||
|
s it skips with truthy closure condition
|
||||||
|
✓ it do not skips with falsy closure condition
|
||||||
|
s it skips with condition and messsage → skipped because foo
|
||||||
|
|
||||||
|
PASS Tests\Features\Test
|
||||||
|
✓ a test
|
||||||
|
✓ higher order message test
|
||||||
|
|
||||||
|
PASS Tests\Fixtures\DirectoryWithTests\ExampleTest
|
||||||
|
✓ it example
|
||||||
|
|
||||||
|
PASS Tests\Fixtures\ExampleTest
|
||||||
|
✓ it example
|
||||||
|
|
||||||
|
PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory
|
||||||
|
✓ closure was bound to custom test case
|
||||||
|
|
||||||
|
PASS Tests\PHPUnit\CustomTestCaseInSubFolders\SubFolder\SubFolder\UsesPerSubDirectory
|
||||||
|
✓ closure was bound to custom test case
|
||||||
|
|
||||||
|
PASS Tests\PHPUnit\CustomTestCaseInSubFolders\SubFolder2\UsesPerFile
|
||||||
|
✓ custom traits can be used
|
||||||
|
✓ trait applied in this file
|
||||||
|
|
||||||
|
PASS Tests\Playground
|
||||||
|
✓ basic
|
||||||
|
|
||||||
|
PASS Tests\Unit\Actions\AddsCoverage
|
||||||
|
✓ it adds coverage if --coverage exist
|
||||||
|
✓ it adds coverage if --min exist
|
||||||
|
|
||||||
|
PASS Tests\Unit\Actions\AddsDefaults
|
||||||
|
✓ it sets defaults
|
||||||
|
✓ it does not override options
|
||||||
|
|
||||||
|
PASS Tests\Unit\Actions\AddsTests
|
||||||
|
✓ default php unit tests
|
||||||
|
✓ it removes warnings
|
||||||
|
|
||||||
|
PASS Tests\Unit\Actions\ValidatesConfiguration
|
||||||
|
✓ it throws exception when configuration not found
|
||||||
|
✓ it throws exception when `process isolation` is true
|
||||||
|
✓ it do not throws exception when `process isolation` is false
|
||||||
|
|
||||||
|
PASS Tests\Unit\Console\Coverage
|
||||||
|
✓ it generates coverage based on file input
|
||||||
|
|
||||||
|
PASS Tests\Unit\Support\Backtrace
|
||||||
|
✓ it gets file name from called file
|
||||||
|
|
||||||
|
PASS Tests\Unit\Support\Reflection
|
||||||
|
✓ it gets file name from closure
|
||||||
|
✓ it gets property values
|
||||||
|
|
||||||
|
PASS Tests\Unit\TestSuite
|
||||||
|
✓ it does not allow to add the same test description twice
|
||||||
|
|
||||||
|
PASS Tests\Visual\SingleTestOrDirectory
|
||||||
|
✓ allows to run a single test
|
||||||
|
✓ allows to run a directory
|
||||||
|
|
||||||
|
WARN Tests\Visual\Success
|
||||||
|
s visual snapshot of test suite on success
|
||||||
|
|
||||||
|
Tests: 6 skipped, 65 passed
|
||||||
|
Time: 2.50s
|
||||||
5
tests/Autoload.php
Normal file
5
tests/Autoload.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (class_exists(NunoMaduro\Collision\Provider::class)) {
|
||||||
|
(new NunoMaduro\Collision\Provider())->register();
|
||||||
|
}
|
||||||
15
tests/Datasets/Numbers.php
Normal file
15
tests/Datasets/Numbers.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
dataset('numbers.closure', function () {
|
||||||
|
yield [1];
|
||||||
|
yield [2];
|
||||||
|
});
|
||||||
|
|
||||||
|
dataset('numbers.closure.wrapped', function () {
|
||||||
|
yield 1;
|
||||||
|
yield 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
dataset('numbers.array', [[1], [2]]);
|
||||||
|
|
||||||
|
dataset('numbers.array.wrapped', [1, 2]);
|
||||||
15
tests/Features/AfterAll.php
Normal file
15
tests/Features/AfterAll.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$file = __DIR__ . DIRECTORY_SEPARATOR . 'after-all-test';
|
||||||
|
|
||||||
|
afterAll(function () use ($file) {
|
||||||
|
unlink($file);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes file after all', function () use ($file) {
|
||||||
|
file_put_contents($file, 'foo');
|
||||||
|
assertFileExists($file);
|
||||||
|
register_shutdown_function(function () use ($file) {
|
||||||
|
assertFileNotExists($file);
|
||||||
|
});
|
||||||
|
});
|
||||||
20
tests/Features/AfterEach.php
Normal file
20
tests/Features/AfterEach.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$state = new stdClass();
|
||||||
|
|
||||||
|
beforeEach(function () use ($state) {
|
||||||
|
$this->state = $state;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () use ($state) {
|
||||||
|
$this->state->bar = 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not get executed before the test', function () {
|
||||||
|
assertFalse(property_exists($this->state, 'bar'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets executed after the test', function () {
|
||||||
|
assertTrue(property_exists($this->state, 'bar'));
|
||||||
|
assertEquals(2, $this->state->bar);
|
||||||
|
});
|
||||||
18
tests/Features/BeforeAll.php
Normal file
18
tests/Features/BeforeAll.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$foo = new \stdClass();
|
||||||
|
$foo->bar = 0;
|
||||||
|
|
||||||
|
beforeAll(function () use ($foo) {
|
||||||
|
$foo->bar++;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets executed before tests', function () use ($foo) {
|
||||||
|
assertEquals($foo->bar, 1);
|
||||||
|
|
||||||
|
$foo->bar = 'changed';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('do not get executed before each test', function () use ($foo) {
|
||||||
|
assertEquals($foo->bar, 'changed');
|
||||||
|
});
|
||||||
15
tests/Features/BeforeEach.php
Normal file
15
tests/Features/BeforeEach.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->bar = 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets executed before each test', function () {
|
||||||
|
assertEquals($this->bar, 2);
|
||||||
|
|
||||||
|
$this->bar = 'changed';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets executed before each test once again', function () {
|
||||||
|
assertEquals($this->bar, 2);
|
||||||
|
});
|
||||||
108
tests/Features/Datasets.php
Normal file
108
tests/Features/Datasets.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Pest\Datasets;
|
||||||
|
use Pest\Exceptions\DatasetAlreadyExist;
|
||||||
|
use Pest\Exceptions\DatasetDoesNotExist;
|
||||||
|
|
||||||
|
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']);`.");
|
||||||
|
Datasets::get('first');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws exception if dataset already exist', function () {
|
||||||
|
Datasets::set('second', [[]]);
|
||||||
|
$this->expectException(DatasetAlreadyExist::class);
|
||||||
|
$this->expectExceptionMessage('A dataset with the name `second` already exist.');
|
||||||
|
Datasets::set('second', [[]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets closures', function () {
|
||||||
|
Datasets::set('foo', function () {
|
||||||
|
yield [1];
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals([[1]], iterator_to_array(Datasets::get('foo')()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets arrays', function () {
|
||||||
|
Datasets::set('bar', [[2]]);
|
||||||
|
|
||||||
|
assertEquals([[2]], Datasets::get('bar'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets bound to test case object', function () {
|
||||||
|
$this->assertTrue(true);
|
||||||
|
})->with([['a'], ['b']]);
|
||||||
|
|
||||||
|
test('it truncates the description', function () {
|
||||||
|
assertTrue(true);
|
||||||
|
// it gets tested by the integration test
|
||||||
|
})->with([str_repeat('Fooo', 10000000)]);
|
||||||
|
|
||||||
|
$state = new stdClass();
|
||||||
|
$state->text = '';
|
||||||
|
|
||||||
|
$datasets = [[1], [2]];
|
||||||
|
|
||||||
|
test('lazy datasets', function ($text) use ($state, $datasets) {
|
||||||
|
$state->text .= $text;
|
||||||
|
assertTrue(in_array([$text], $datasets));
|
||||||
|
})->with($datasets);
|
||||||
|
|
||||||
|
test('lazy datasets did the job right', function () use ($state) {
|
||||||
|
assertEquals('12', $state->text);
|
||||||
|
});
|
||||||
|
|
||||||
|
$state->text = '';
|
||||||
|
|
||||||
|
test('eager datasets', function ($text) use ($state, $datasets) {
|
||||||
|
$state->text .= $text;
|
||||||
|
assertTrue(in_array([$text], $datasets));
|
||||||
|
})->with(function () use ($datasets) {
|
||||||
|
return $datasets;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eager datasets did the job right', function () use ($state) {
|
||||||
|
assertEquals('1212', $state->text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lazy registered datasets', function ($text) use ($state, $datasets) {
|
||||||
|
$state->text .= $text;
|
||||||
|
assertTrue(in_array([$text], $datasets));
|
||||||
|
})->with('numbers.array');
|
||||||
|
|
||||||
|
test('lazy registered datasets did the job right', function () use ($state) {
|
||||||
|
assertEquals('121212', $state->text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eager registered datasets', function ($text) use ($state, $datasets) {
|
||||||
|
$state->text .= $text;
|
||||||
|
assertTrue(in_array([$text], $datasets));
|
||||||
|
})->with('numbers.closure');
|
||||||
|
|
||||||
|
test('eager registered datasets did the job right', function () use ($state) {
|
||||||
|
assertEquals('12121212', $state->text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eager wrapped registered datasets', function ($text) use ($state, $datasets) {
|
||||||
|
$state->text .= $text;
|
||||||
|
assertTrue(in_array([$text], $datasets));
|
||||||
|
})->with('numbers.closure.wrapped');
|
||||||
|
|
||||||
|
test('eager registered wrapped datasets did the job right', function () use ($state) {
|
||||||
|
assertEquals('1212121212', $state->text);
|
||||||
|
});
|
||||||
|
|
||||||
|
class Bar
|
||||||
|
{
|
||||||
|
public $name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$namedDatasets = [
|
||||||
|
new Bar(),
|
||||||
|
];
|
||||||
|
|
||||||
|
test('lazy named datasets', function ($text) use ($state, $datasets) {
|
||||||
|
assertTrue(true);
|
||||||
|
})->with($namedDatasets);
|
||||||
15
tests/Features/Exceptions.php
Normal file
15
tests/Features/Exceptions.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('gives access the the underlying expectException', function () {
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|
||||||
|
throw new InvalidArgumentException();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('catch exceptions', function () {
|
||||||
|
throw new Exception('Something bad happened');
|
||||||
|
})->throws(Exception::class);
|
||||||
|
|
||||||
|
it('catch exceptions and messages', function () {
|
||||||
|
throw new Exception('Something bad happened');
|
||||||
|
})->throws(Exception::class, 'Something bad happened');
|
||||||
7
tests/Features/HigherOrderMessages.php
Normal file
7
tests/Features/HigherOrderMessages.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
beforeEach()->assertTrue(true);
|
||||||
|
|
||||||
|
it('proxies calls to object')->assertTrue(true);
|
||||||
|
|
||||||
|
afterEach()->assertTrue(true);
|
||||||
7
tests/Features/It.php
Normal file
7
tests/Features/It.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('is a test', function () {
|
||||||
|
assertArrayHasKey('key', ['key' => 'foo']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a higher order message test')->assertTrue(true);
|
||||||
15
tests/Features/Mocks.php
Normal file
15
tests/Features/Mocks.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
interface Foo
|
||||||
|
{
|
||||||
|
public function bar(): int;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('has bar', function () {
|
||||||
|
$mock = Mockery::mock(Foo::class);
|
||||||
|
$mock->shouldReceive('bar')
|
||||||
|
->times(1)
|
||||||
|
->andReturn(2);
|
||||||
|
|
||||||
|
assertEquals(2, $mock->bar());
|
||||||
|
});
|
||||||
29
tests/Features/Skip.php
Normal file
29
tests/Features/Skip.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('do not skips')
|
||||||
|
->skip(false)
|
||||||
|
->assertTrue(true);
|
||||||
|
|
||||||
|
it('skips with truthy')
|
||||||
|
->skip(1)
|
||||||
|
->assertTrue(false);
|
||||||
|
|
||||||
|
it('skips with truthy condition by default')
|
||||||
|
->skip()
|
||||||
|
->assertTrue(false);
|
||||||
|
|
||||||
|
it('skips with message')
|
||||||
|
->skip('skipped because bar')
|
||||||
|
->assertTrue(false);
|
||||||
|
|
||||||
|
it('skips with truthy closure condition')
|
||||||
|
->skip(function () { return '1'; })
|
||||||
|
->assertTrue(false);
|
||||||
|
|
||||||
|
it('do not skips with falsy closure condition')
|
||||||
|
->skip(function () { return false; })
|
||||||
|
->assertTrue(true);
|
||||||
|
|
||||||
|
it('skips with condition and messsage')
|
||||||
|
->skip(true, 'skipped because foo')
|
||||||
|
->assertTrue(false);
|
||||||
7
tests/Features/Test.php
Normal file
7
tests/Features/Test.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('a test', function () {
|
||||||
|
assertArrayHasKey('key', ['key' => 'foo']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('higher order message test')->assertTrue(true);
|
||||||
27
tests/Features/TestCycle.php
Normal file
27
tests/Features/TestCycle.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$foo = new stdClass();
|
||||||
|
$foo->beforeAll = false;
|
||||||
|
$foo->beforeEach = false;
|
||||||
|
$foo->afterEach = false;
|
||||||
|
$foo->afterAll = false;
|
||||||
|
|
||||||
|
beforeAll(function () {
|
||||||
|
$foo->beforeAll = true;
|
||||||
|
});
|
||||||
|
beforeEach(function () {
|
||||||
|
$foo->beforeEach = true;
|
||||||
|
});
|
||||||
|
afterEach(function () {
|
||||||
|
$foo->afterEach = true;
|
||||||
|
});
|
||||||
|
afterAll(function () {
|
||||||
|
$foo->afterAll = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
register_shutdown_function(function () use ($foo) {
|
||||||
|
assertFalse($foo->beforeAll);
|
||||||
|
assertFalse($foo->beforeEach);
|
||||||
|
assertFalse($foo->afterEach);
|
||||||
|
assertFalse($foo->afterAll);
|
||||||
|
});
|
||||||
3
tests/Fixtures/DirectoryWithTests/ExampleTest.php
Normal file
3
tests/Fixtures/DirectoryWithTests/ExampleTest.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('example')->assertTrue(true);
|
||||||
3
tests/Fixtures/ExampleTest.php
Normal file
3
tests/Fixtures/ExampleTest.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
it('example')->assertTrue(true);
|
||||||
8
tests/Fixtures/phpunit-in-isolation.xml
Normal file
8
tests/Fixtures/phpunit-in-isolation.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
processIsolation="true"
|
||||||
|
>
|
||||||
|
</phpunit>
|
||||||
8
tests/Fixtures/phpunit-not-in-isolation.xml
Normal file
8
tests/Fixtures/phpunit-not-in-isolation.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
processIsolation="false"
|
||||||
|
>
|
||||||
|
</phpunit>
|
||||||
16
tests/PHPUnit/CustomTestCase/CustomTestCase.php
Normal file
16
tests/PHPUnit/CustomTestCase/CustomTestCase.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\CustomTestCase;
|
||||||
|
|
||||||
|
use function PHPUnit\Framework\assertTrue;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CustomTestCase extends TestCase
|
||||||
|
{
|
||||||
|
public function assertCustomTrue()
|
||||||
|
{
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
tests/PHPUnit/CustomTestCase/PhpunitTest.php
Normal file
23
tests/PHPUnit/CustomTestCase/PhpunitTest.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\CustomTestCase;
|
||||||
|
|
||||||
|
use function PHPUnit\Framework\assertTrue;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class PhpunitTest extends TestCase
|
||||||
|
{
|
||||||
|
public static $executed = false;
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function testThatGetsExecuted(): void
|
||||||
|
{
|
||||||
|
self::$executed = true;
|
||||||
|
|
||||||
|
$this->assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register_shutdown_function(fn () => assertTrue(PhpunitTest::$executed));
|
||||||
7
tests/PHPUnit/CustomTestCase/UsesPerDirectory.php
Normal file
7
tests/PHPUnit/CustomTestCase/UsesPerDirectory.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
uses(Tests\CustomTestCase\CustomTestCase::class)->in(__DIR__);
|
||||||
|
|
||||||
|
test('closure was bound to CustomTestCase', function () {
|
||||||
|
$this->assertCustomTrue();
|
||||||
|
});
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\SubFolder\SubFolder\SubFolder;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CustomTestCaseInSubFolder extends TestCase
|
||||||
|
{
|
||||||
|
public function assertCustomInSubFolderTrue()
|
||||||
|
{
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
test('closure was bound to CustomTestCase', function () {
|
||||||
|
$this->assertCustomInSubFolderTrue();
|
||||||
|
});
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
trait MyCustomTrait
|
||||||
|
{
|
||||||
|
public function assertFalseIsFalse()
|
||||||
|
{
|
||||||
|
assertFalse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyCustomClass extends PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function assertTrueIsTrue()
|
||||||
|
{
|
||||||
|
assertTrue(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uses(MyCustomClass::class, MyCustomTrait::class);
|
||||||
|
|
||||||
|
test('custom traits can be used', function () {
|
||||||
|
$this->assertTrueIsTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trait applied in this file')->assertTrueIsTrue();
|
||||||
3
tests/PHPUnit/Pest.php
Normal file
3
tests/PHPUnit/Pest.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
uses(Tests\SubFolder\SubFolder\SubFolder\CustomTestCaseInSubFolder::class)->in('CustomTestCaseInSubFolders/SubFolder');
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user