diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..79c3e0d88 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f19490bb8 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..d03cb24dc --- /dev/null +++ b/.github/FUNDING.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fd6fa1c2d --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..69a8e2d8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea/* +.idea/codeStyleSettings.xml +composer.lock +/vendor/ +coverage.xml +.phpunit.result.cache +.php_cs.cache +.temp/coverage.php +*.swp +*.swo diff --git a/.php_cs b/.php_cs new file mode 100644 index 000000000..725a30350 --- /dev/null +++ b/.php_cs @@ -0,0 +1,32 @@ +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); diff --git a/.temp/.gitkeep b/.temp/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..52909de3f --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..368218996 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 +``` diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 000000000..14b90ed40 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Nuno Maduro + +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. diff --git a/README.md b/README.md new file mode 100644 index 000000000..318bdf9cd --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +

+ PEST Preview +

+ Build Status + Total Downloads + Latest Version + License +

+

+ +------ +**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)**. diff --git a/bin/pest b/bin/pest new file mode 100755 index 000000000..10e61a14b --- /dev/null +++ b/bin/pest @@ -0,0 +1,31 @@ +#!/usr/bin/env php +register(); + + $rootPath = getcwd(); + + $testSuite = TestSuite::getInstance($rootPath); + + ValidatesEnvironment::in($testSuite); + + exit((new Command($testSuite, new ConsoleOutput()))->run($_SERVER['argv'])); +})(); diff --git a/compiled/.gitkeep b/compiled/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/compiled/globals.php b/compiled/globals.php new file mode 100644 index 000000000..9f82a8428 --- /dev/null +++ b/compiled/globals.php @@ -0,0 +1,1926 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\Constraint\Constraint; + +/** + * Asserts that an array has a specified key. + * + * @param int|string $key + * @param array|\ArrayAccess $array + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertArrayHasKey + */ +function assertArrayHasKey($key, $array, string $message = ''): void +{ + Assert::assertArrayHasKey(...\func_get_args()); +} + +/** + * Asserts that an array does not have a specified key. + * + * @param int|string $key + * @param array|\ArrayAccess $array + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertArrayNotHasKey + */ +function assertArrayNotHasKey($key, $array, string $message = ''): void +{ + Assert::assertArrayNotHasKey(...\func_get_args()); +} + +/** + * Asserts that a haystack contains a needle. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertContains + */ +function assertContains($needle, iterable $haystack, string $message = ''): void +{ + Assert::assertContains(...\func_get_args()); +} + +function assertContainsEquals($needle, iterable $haystack, string $message = ''): void +{ + Assert::assertContainsEquals(...\func_get_args()); +} + +/** + * Asserts that a haystack does not contain a needle. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertNotContains + */ +function assertNotContains($needle, iterable $haystack, string $message = ''): void +{ + Assert::assertNotContains(...\func_get_args()); +} + +function assertNotContainsEquals($needle, iterable $haystack, string $message = ''): void +{ + Assert::assertNotContainsEquals(...\func_get_args()); +} + +/** + * Asserts that a haystack contains only values of a given type. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertContainsOnly + */ +function assertContainsOnly(string $type, iterable $haystack, ?bool $isNativeType = null, string $message = ''): void +{ + Assert::assertContainsOnly(...\func_get_args()); +} + +/** + * Asserts that a haystack contains only instances of a given class name. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertContainsOnlyInstancesOf + */ +function assertContainsOnlyInstancesOf(string $className, iterable $haystack, string $message = ''): void +{ + Assert::assertContainsOnlyInstancesOf(...\func_get_args()); +} + +/** + * Asserts that a haystack does not contain only values of a given type. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNotContainsOnly + */ +function assertNotContainsOnly(string $type, iterable $haystack, ?bool $isNativeType = null, string $message = ''): void +{ + Assert::assertNotContainsOnly(...\func_get_args()); +} + +/** + * Asserts the number of elements of an array, Countable or Traversable. + * + * @param \Countable|iterable $haystack + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertCount + */ +function assertCount(int $expectedCount, $haystack, string $message = ''): void +{ + Assert::assertCount(...\func_get_args()); +} + +/** + * Asserts the number of elements of an array, Countable or Traversable. + * + * @param \Countable|iterable $haystack + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertNotCount + */ +function assertNotCount(int $expectedCount, $haystack, string $message = ''): void +{ + Assert::assertNotCount(...\func_get_args()); +} + +/** + * Asserts that two variables are equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertEquals + */ +function assertEquals($expected, $actual, string $message = ''): void +{ + Assert::assertEquals(...\func_get_args()); +} + +/** + * Asserts that two variables are equal (canonicalizing). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertEqualsCanonicalizing + */ +function assertEqualsCanonicalizing($expected, $actual, string $message = ''): void +{ + Assert::assertEqualsCanonicalizing(...\func_get_args()); +} + +/** + * Asserts that two variables are equal (ignoring case). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertEqualsIgnoringCase + */ +function assertEqualsIgnoringCase($expected, $actual, string $message = ''): void +{ + Assert::assertEqualsIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that two variables are equal (with delta). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertEqualsWithDelta + */ +function assertEqualsWithDelta($expected, $actual, float $delta, string $message = ''): void +{ + Assert::assertEqualsWithDelta(...\func_get_args()); +} + +/** + * Asserts that two variables are not equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNotEquals + */ +function assertNotEquals($expected, $actual, string $message = ''): void +{ + Assert::assertNotEquals(...\func_get_args()); +} + +/** + * Asserts that two variables are not equal (canonicalizing). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNotEqualsCanonicalizing + */ +function assertNotEqualsCanonicalizing($expected, $actual, string $message = ''): void +{ + Assert::assertNotEqualsCanonicalizing(...\func_get_args()); +} + +/** + * Asserts that two variables are not equal (ignoring case). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNotEqualsIgnoringCase + */ +function assertNotEqualsIgnoringCase($expected, $actual, string $message = ''): void +{ + Assert::assertNotEqualsIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that two variables are not equal (with delta). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNotEqualsWithDelta + */ +function assertNotEqualsWithDelta($expected, $actual, float $delta, string $message = ''): void +{ + Assert::assertNotEqualsWithDelta(...\func_get_args()); +} + +/** + * Asserts that a variable is empty. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert empty $actual + * + * @see Assert::assertEmpty + */ +function assertEmpty($actual, string $message = ''): void +{ + Assert::assertEmpty(...\func_get_args()); +} + +/** + * Asserts that a variable is not empty. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !empty $actual + * + * @see Assert::assertNotEmpty + */ +function assertNotEmpty($actual, string $message = ''): void +{ + Assert::assertNotEmpty(...\func_get_args()); +} + +/** + * Asserts that a value is greater than another value. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertGreaterThan + */ +function assertGreaterThan($expected, $actual, string $message = ''): void +{ + Assert::assertGreaterThan(...\func_get_args()); +} + +/** + * Asserts that a value is greater than or equal to another value. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertGreaterThanOrEqual + */ +function assertGreaterThanOrEqual($expected, $actual, string $message = ''): void +{ + Assert::assertGreaterThanOrEqual(...\func_get_args()); +} + +/** + * Asserts that a value is smaller than another value. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertLessThan + */ +function assertLessThan($expected, $actual, string $message = ''): void +{ + Assert::assertLessThan(...\func_get_args()); +} + +/** + * Asserts that a value is smaller than or equal to another value. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertLessThanOrEqual + */ +function assertLessThanOrEqual($expected, $actual, string $message = ''): void +{ + Assert::assertLessThanOrEqual(...\func_get_args()); +} + +/** + * Asserts that the contents of one file is equal to the contents of another + * file. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileEquals + */ +function assertFileEquals(string $expected, string $actual, string $message = ''): void +{ + Assert::assertFileEquals(...\func_get_args()); +} + +/** + * Asserts that the contents of one file is equal to the contents of another + * file (canonicalizing). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileEqualsCanonicalizing + */ +function assertFileEqualsCanonicalizing(string $expected, string $actual, string $message = ''): void +{ + Assert::assertFileEqualsCanonicalizing(...\func_get_args()); +} + +/** + * Asserts that the contents of one file is equal to the contents of another + * file (ignoring case). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileEqualsIgnoringCase + */ +function assertFileEqualsIgnoringCase(string $expected, string $actual, string $message = ''): void +{ + Assert::assertFileEqualsIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that the contents of one file is not equal to the contents of + * another file. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileNotEquals + */ +function assertFileNotEquals(string $expected, string $actual, string $message = ''): void +{ + Assert::assertFileNotEquals(...\func_get_args()); +} + +/** + * Asserts that the contents of one file is not equal to the contents of another + * file (canonicalizing). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileNotEqualsCanonicalizing + */ +function assertFileNotEqualsCanonicalizing(string $expected, string $actual, string $message = ''): void +{ + Assert::assertFileNotEqualsCanonicalizing(...\func_get_args()); +} + +/** + * Asserts that the contents of one file is not equal to the contents of another + * file (ignoring case). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileNotEqualsIgnoringCase + */ +function assertFileNotEqualsIgnoringCase(string $expected, string $actual, string $message = ''): void +{ + Assert::assertFileNotEqualsIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that the contents of a string is equal + * to the contents of a file. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringEqualsFile + */ +function assertStringEqualsFile(string $expectedFile, string $actualString, string $message = ''): void +{ + Assert::assertStringEqualsFile(...\func_get_args()); +} + +/** + * Asserts that the contents of a string is equal + * to the contents of a file (canonicalizing). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringEqualsFileCanonicalizing + */ +function assertStringEqualsFileCanonicalizing(string $expectedFile, string $actualString, string $message = ''): void +{ + Assert::assertStringEqualsFileCanonicalizing(...\func_get_args()); +} + +/** + * Asserts that the contents of a string is equal + * to the contents of a file (ignoring case). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringEqualsFileIgnoringCase + */ +function assertStringEqualsFileIgnoringCase(string $expectedFile, string $actualString, string $message = ''): void +{ + Assert::assertStringEqualsFileIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that the contents of a string is not equal + * to the contents of a file. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotEqualsFile + */ +function assertStringNotEqualsFile(string $expectedFile, string $actualString, string $message = ''): void +{ + Assert::assertStringNotEqualsFile(...\func_get_args()); +} + +/** + * Asserts that the contents of a string is not equal + * to the contents of a file (canonicalizing). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotEqualsFileCanonicalizing + */ +function assertStringNotEqualsFileCanonicalizing(string $expectedFile, string $actualString, string $message = ''): void +{ + Assert::assertStringNotEqualsFileCanonicalizing(...\func_get_args()); +} + +/** + * Asserts that the contents of a string is not equal + * to the contents of a file (ignoring case). + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotEqualsFileIgnoringCase + */ +function assertStringNotEqualsFileIgnoringCase(string $expectedFile, string $actualString, string $message = ''): void +{ + Assert::assertStringNotEqualsFileIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that a file/dir is readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertIsReadable + */ +function assertIsReadable(string $filename, string $message = ''): void +{ + Assert::assertIsReadable(...\func_get_args()); +} + +/** + * Asserts that a file/dir exists and is not readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertIsNotReadable + */ +function assertIsNotReadable(string $filename, string $message = ''): void +{ + Assert::assertIsNotReadable(...\func_get_args()); +} + +/** + * Asserts that a file/dir exists and is not readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4062 + * @see Assert::assertNotIsReadable + */ +function assertNotIsReadable(string $filename, string $message = ''): void +{ + Assert::assertNotIsReadable(...\func_get_args()); +} + +/** + * Asserts that a file/dir exists and is writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertIsWritable + */ +function assertIsWritable(string $filename, string $message = ''): void +{ + Assert::assertIsWritable(...\func_get_args()); +} + +/** + * Asserts that a file/dir exists and is not writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertIsNotWritable + */ +function assertIsNotWritable(string $filename, string $message = ''): void +{ + Assert::assertIsNotWritable(...\func_get_args()); +} + +/** + * Asserts that a file/dir exists and is not writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4065 + * @see Assert::assertNotIsWritable + */ +function assertNotIsWritable(string $filename, string $message = ''): void +{ + Assert::assertNotIsWritable(...\func_get_args()); +} + +/** + * Asserts that a directory exists. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDirectoryExists + */ +function assertDirectoryExists(string $directory, string $message = ''): void +{ + Assert::assertDirectoryExists(...\func_get_args()); +} + +/** + * Asserts that a directory does not exist. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDirectoryDoesNotExist + */ +function assertDirectoryDoesNotExist(string $directory, string $message = ''): void +{ + Assert::assertDirectoryDoesNotExist(...\func_get_args()); +} + +/** + * Asserts that a directory does not exist. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4068 + * @see Assert::assertDirectoryNotExists + */ +function assertDirectoryNotExists(string $directory, string $message = ''): void +{ + Assert::assertDirectoryNotExists(...\func_get_args()); +} + +/** + * Asserts that a directory exists and is readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDirectoryIsReadable + */ +function assertDirectoryIsReadable(string $directory, string $message = ''): void +{ + Assert::assertDirectoryIsReadable(...\func_get_args()); +} + +/** + * Asserts that a directory exists and is not readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDirectoryIsNotReadable + */ +function assertDirectoryIsNotReadable(string $directory, string $message = ''): void +{ + Assert::assertDirectoryIsNotReadable(...\func_get_args()); +} + +/** + * Asserts that a directory exists and is not readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4071 + * @see Assert::assertDirectoryNotIsReadable + */ +function assertDirectoryNotIsReadable(string $directory, string $message = ''): void +{ + Assert::assertDirectoryNotIsReadable(...\func_get_args()); +} + +/** + * Asserts that a directory exists and is writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDirectoryIsWritable + */ +function assertDirectoryIsWritable(string $directory, string $message = ''): void +{ + Assert::assertDirectoryIsWritable(...\func_get_args()); +} + +/** + * Asserts that a directory exists and is not writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDirectoryIsNotWritable + */ +function assertDirectoryIsNotWritable(string $directory, string $message = ''): void +{ + Assert::assertDirectoryIsNotWritable(...\func_get_args()); +} + +/** + * Asserts that a directory exists and is not writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4074 + * @see Assert::assertDirectoryNotIsWritable + */ +function assertDirectoryNotIsWritable(string $directory, string $message = ''): void +{ + Assert::assertDirectoryNotIsWritable(...\func_get_args()); +} + +/** + * Asserts that a file exists. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileExists + */ +function assertFileExists(string $filename, string $message = ''): void +{ + Assert::assertFileExists(...\func_get_args()); +} + +/** + * Asserts that a file does not exist. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileDoesNotExist + */ +function assertFileDoesNotExist(string $filename, string $message = ''): void +{ + Assert::assertFileDoesNotExist(...\func_get_args()); +} + +/** + * Asserts that a file does not exist. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4077 + * @see Assert::assertFileNotExists + */ +function assertFileNotExists(string $filename, string $message = ''): void +{ + Assert::assertFileNotExists(...\func_get_args()); +} + +/** + * Asserts that a file exists and is readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileIsReadable + */ +function assertFileIsReadable(string $file, string $message = ''): void +{ + Assert::assertFileIsReadable(...\func_get_args()); +} + +/** + * Asserts that a file exists and is not readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileIsNotReadable + */ +function assertFileIsNotReadable(string $file, string $message = ''): void +{ + Assert::assertFileIsNotReadable(...\func_get_args()); +} + +/** + * Asserts that a file exists and is not readable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4080 + * @see Assert::assertFileNotIsReadable + */ +function assertFileNotIsReadable(string $file, string $message = ''): void +{ + Assert::assertFileNotIsReadable(...\func_get_args()); +} + +/** + * Asserts that a file exists and is writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileIsWritable + */ +function assertFileIsWritable(string $file, string $message = ''): void +{ + Assert::assertFileIsWritable(...\func_get_args()); +} + +/** + * Asserts that a file exists and is not writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFileIsNotWritable + */ +function assertFileIsNotWritable(string $file, string $message = ''): void +{ + Assert::assertFileIsNotWritable(...\func_get_args()); +} + +/** + * Asserts that a file exists and is not writable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4083 + * @see Assert::assertFileNotIsWritable + */ +function assertFileNotIsWritable(string $file, string $message = ''): void +{ + Assert::assertFileNotIsWritable(...\func_get_args()); +} + +/** + * Asserts that a condition is true. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert true $condition + * + * @see Assert::assertTrue + */ +function assertTrue($condition, string $message = ''): void +{ + Assert::assertTrue(...\func_get_args()); +} + +/** + * Asserts that a condition is not true. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !true $condition + * + * @see Assert::assertNotTrue + */ +function assertNotTrue($condition, string $message = ''): void +{ + Assert::assertNotTrue(...\func_get_args()); +} + +/** + * Asserts that a condition is false. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert false $condition + * + * @see Assert::assertFalse + */ +function assertFalse($condition, string $message = ''): void +{ + Assert::assertFalse(...\func_get_args()); +} + +/** + * Asserts that a condition is not false. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !false $condition + * + * @see Assert::assertNotFalse + */ +function assertNotFalse($condition, string $message = ''): void +{ + Assert::assertNotFalse(...\func_get_args()); +} + +/** + * Asserts that a variable is null. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert null $actual + * + * @see Assert::assertNull + */ +function assertNull($actual, string $message = ''): void +{ + Assert::assertNull(...\func_get_args()); +} + +/** + * Asserts that a variable is not null. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !null $actual + * + * @see Assert::assertNotNull + */ +function assertNotNull($actual, string $message = ''): void +{ + Assert::assertNotNull(...\func_get_args()); +} + +/** + * Asserts that a variable is finite. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertFinite + */ +function assertFinite($actual, string $message = ''): void +{ + Assert::assertFinite(...\func_get_args()); +} + +/** + * Asserts that a variable is infinite. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertInfinite + */ +function assertInfinite($actual, string $message = ''): void +{ + Assert::assertInfinite(...\func_get_args()); +} + +/** + * Asserts that a variable is nan. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNan + */ +function assertNan($actual, string $message = ''): void +{ + Assert::assertNan(...\func_get_args()); +} + +/** + * Asserts that a class has a specified attribute. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertClassHasAttribute + */ +function assertClassHasAttribute(string $attributeName, string $className, string $message = ''): void +{ + Assert::assertClassHasAttribute(...\func_get_args()); +} + +/** + * Asserts that a class does not have a specified attribute. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertClassNotHasAttribute + */ +function assertClassNotHasAttribute(string $attributeName, string $className, string $message = ''): void +{ + Assert::assertClassNotHasAttribute(...\func_get_args()); +} + +/** + * Asserts that a class has a specified static attribute. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertClassHasStaticAttribute + */ +function assertClassHasStaticAttribute(string $attributeName, string $className, string $message = ''): void +{ + Assert::assertClassHasStaticAttribute(...\func_get_args()); +} + +/** + * Asserts that a class does not have a specified static attribute. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertClassNotHasStaticAttribute + */ +function assertClassNotHasStaticAttribute(string $attributeName, string $className, string $message = ''): void +{ + Assert::assertClassNotHasStaticAttribute(...\func_get_args()); +} + +/** + * Asserts that an object has a specified attribute. + * + * @param object $object + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertObjectHasAttribute + */ +function assertObjectHasAttribute(string $attributeName, $object, string $message = ''): void +{ + Assert::assertObjectHasAttribute(...\func_get_args()); +} + +/** + * Asserts that an object does not have a specified attribute. + * + * @param object $object + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertObjectNotHasAttribute + */ +function assertObjectNotHasAttribute(string $attributeName, $object, string $message = ''): void +{ + Assert::assertObjectNotHasAttribute(...\func_get_args()); +} + +/** + * Asserts that two variables have the same type and value. + * Used on objects, it asserts that two variables reference + * the same object. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-template ExpectedType + * @psalm-param ExpectedType $expected + * @psalm-assert =ExpectedType $actual + * + * @see Assert::assertSame + */ +function assertSame($expected, $actual, string $message = ''): void +{ + Assert::assertSame(...\func_get_args()); +} + +/** + * Asserts that two variables do not have the same type and value. + * Used on objects, it asserts that two variables do not reference + * the same object. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertNotSame + */ +function assertNotSame($expected, $actual, string $message = ''): void +{ + Assert::assertNotSame(...\func_get_args()); +} + +/** + * Asserts that a variable is of a given type. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @psalm-template ExpectedType of object + * @psalm-param class-string $expected + * @psalm-assert ExpectedType $actual + * + * @see Assert::assertInstanceOf + */ +function assertInstanceOf(string $expected, $actual, string $message = ''): void +{ + Assert::assertInstanceOf(...\func_get_args()); +} + +/** + * Asserts that a variable is not of a given type. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @psalm-template ExpectedType of object + * @psalm-param class-string $expected + * @psalm-assert !ExpectedType $actual + * + * @see Assert::assertNotInstanceOf + */ +function assertNotInstanceOf(string $expected, $actual, string $message = ''): void +{ + Assert::assertNotInstanceOf(...\func_get_args()); +} + +/** + * Asserts that a variable is of type array. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert array $actual + * + * @see Assert::assertIsArray + */ +function assertIsArray($actual, string $message = ''): void +{ + Assert::assertIsArray(...\func_get_args()); +} + +/** + * Asserts that a variable is of type bool. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert bool $actual + * + * @see Assert::assertIsBool + */ +function assertIsBool($actual, string $message = ''): void +{ + Assert::assertIsBool(...\func_get_args()); +} + +/** + * Asserts that a variable is of type float. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert float $actual + * + * @see Assert::assertIsFloat + */ +function assertIsFloat($actual, string $message = ''): void +{ + Assert::assertIsFloat(...\func_get_args()); +} + +/** + * Asserts that a variable is of type int. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert int $actual + * + * @see Assert::assertIsInt + */ +function assertIsInt($actual, string $message = ''): void +{ + Assert::assertIsInt(...\func_get_args()); +} + +/** + * Asserts that a variable is of type numeric. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert numeric $actual + * + * @see Assert::assertIsNumeric + */ +function assertIsNumeric($actual, string $message = ''): void +{ + Assert::assertIsNumeric(...\func_get_args()); +} + +/** + * Asserts that a variable is of type object. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert object $actual + * + * @see Assert::assertIsObject + */ +function assertIsObject($actual, string $message = ''): void +{ + Assert::assertIsObject(...\func_get_args()); +} + +/** + * Asserts that a variable is of type resource. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert resource $actual + * + * @see Assert::assertIsResource + */ +function assertIsResource($actual, string $message = ''): void +{ + Assert::assertIsResource(...\func_get_args()); +} + +/** + * Asserts that a variable is of type string. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert string $actual + * + * @see Assert::assertIsString + */ +function assertIsString($actual, string $message = ''): void +{ + Assert::assertIsString(...\func_get_args()); +} + +/** + * Asserts that a variable is of type scalar. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert scalar $actual + * + * @see Assert::assertIsScalar + */ +function assertIsScalar($actual, string $message = ''): void +{ + Assert::assertIsScalar(...\func_get_args()); +} + +/** + * Asserts that a variable is of type callable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert callable $actual + * + * @see Assert::assertIsCallable + */ +function assertIsCallable($actual, string $message = ''): void +{ + Assert::assertIsCallable(...\func_get_args()); +} + +/** + * Asserts that a variable is of type iterable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert iterable $actual + * + * @see Assert::assertIsIterable + */ +function assertIsIterable($actual, string $message = ''): void +{ + Assert::assertIsIterable(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type array. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !array $actual + * + * @see Assert::assertIsNotArray + */ +function assertIsNotArray($actual, string $message = ''): void +{ + Assert::assertIsNotArray(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type bool. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !bool $actual + * + * @see Assert::assertIsNotBool + */ +function assertIsNotBool($actual, string $message = ''): void +{ + Assert::assertIsNotBool(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type float. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !float $actual + * + * @see Assert::assertIsNotFloat + */ +function assertIsNotFloat($actual, string $message = ''): void +{ + Assert::assertIsNotFloat(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type int. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !int $actual + * + * @see Assert::assertIsNotInt + */ +function assertIsNotInt($actual, string $message = ''): void +{ + Assert::assertIsNotInt(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type numeric. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !numeric $actual + * + * @see Assert::assertIsNotNumeric + */ +function assertIsNotNumeric($actual, string $message = ''): void +{ + Assert::assertIsNotNumeric(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type object. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !object $actual + * + * @see Assert::assertIsNotObject + */ +function assertIsNotObject($actual, string $message = ''): void +{ + Assert::assertIsNotObject(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type resource. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !resource $actual + * + * @see Assert::assertIsNotResource + */ +function assertIsNotResource($actual, string $message = ''): void +{ + Assert::assertIsNotResource(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type string. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !string $actual + * + * @see Assert::assertIsNotString + */ +function assertIsNotString($actual, string $message = ''): void +{ + Assert::assertIsNotString(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type scalar. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !scalar $actual + * + * @see Assert::assertIsNotScalar + */ +function assertIsNotScalar($actual, string $message = ''): void +{ + Assert::assertIsNotScalar(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type callable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !callable $actual + * + * @see Assert::assertIsNotCallable + */ +function assertIsNotCallable($actual, string $message = ''): void +{ + Assert::assertIsNotCallable(...\func_get_args()); +} + +/** + * Asserts that a variable is not of type iterable. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @psalm-assert !iterable $actual + * + * @see Assert::assertIsNotIterable + */ +function assertIsNotIterable($actual, string $message = ''): void +{ + Assert::assertIsNotIterable(...\func_get_args()); +} + +/** + * Asserts that a string matches a given regular expression. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertMatchesRegularExpression + */ +function assertMatchesRegularExpression(string $pattern, string $string, string $message = ''): void +{ + Assert::assertMatchesRegularExpression(...\func_get_args()); +} + +/** + * Asserts that a string matches a given regular expression. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4086 + * @see Assert::assertRegExp + */ +function assertRegExp(string $pattern, string $string, string $message = ''): void +{ + Assert::assertRegExp(...\func_get_args()); +} + +/** + * Asserts that a string does not match a given regular expression. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertDoesNotMatchRegularExpression + */ +function assertDoesNotMatchRegularExpression(string $pattern, string $string, string $message = ''): void +{ + Assert::assertDoesNotMatchRegularExpression(...\func_get_args()); +} + +/** + * Asserts that a string does not match a given regular expression. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4089 + * @see Assert::assertNotRegExp + */ +function assertNotRegExp(string $pattern, string $string, string $message = ''): void +{ + Assert::assertNotRegExp(...\func_get_args()); +} + +/** + * Assert that the size of two arrays (or `Countable` or `Traversable` objects) + * is the same. + * + * @param \Countable|iterable $expected + * @param \Countable|iterable $actual + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertSameSize + */ +function assertSameSize($expected, $actual, string $message = ''): void +{ + Assert::assertSameSize(...\func_get_args()); +} + +/** + * Assert that the size of two arrays (or `Countable` or `Traversable` objects) + * is not the same. + * + * @param \Countable|iterable $expected + * @param \Countable|iterable $actual + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertNotSameSize + */ +function assertNotSameSize($expected, $actual, string $message = ''): void +{ + Assert::assertNotSameSize(...\func_get_args()); +} + +/** + * Asserts that a string matches a given format string. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringMatchesFormat + */ +function assertStringMatchesFormat(string $format, string $string, string $message = ''): void +{ + Assert::assertStringMatchesFormat(...\func_get_args()); +} + +/** + * Asserts that a string does not match a given format string. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotMatchesFormat + */ +function assertStringNotMatchesFormat(string $format, string $string, string $message = ''): void +{ + Assert::assertStringNotMatchesFormat(...\func_get_args()); +} + +/** + * Asserts that a string matches a given format file. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringMatchesFormatFile + */ +function assertStringMatchesFormatFile(string $formatFile, string $string, string $message = ''): void +{ + Assert::assertStringMatchesFormatFile(...\func_get_args()); +} + +/** + * Asserts that a string does not match a given format string. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotMatchesFormatFile + */ +function assertStringNotMatchesFormatFile(string $formatFile, string $string, string $message = ''): void +{ + Assert::assertStringNotMatchesFormatFile(...\func_get_args()); +} + +/** + * Asserts that a string starts with a given prefix. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringStartsWith + */ +function assertStringStartsWith(string $prefix, string $string, string $message = ''): void +{ + Assert::assertStringStartsWith(...\func_get_args()); +} + +/** + * Asserts that a string starts not with a given prefix. + * + * @param string $prefix + * @param string $string + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringStartsNotWith + */ +function assertStringStartsNotWith($prefix, $string, string $message = ''): void +{ + Assert::assertStringStartsNotWith(...\func_get_args()); +} + +/** + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringContainsString + */ +function assertStringContainsString(string $needle, string $haystack, string $message = ''): void +{ + Assert::assertStringContainsString(...\func_get_args()); +} + +/** + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringContainsStringIgnoringCase + */ +function assertStringContainsStringIgnoringCase(string $needle, string $haystack, string $message = ''): void +{ + Assert::assertStringContainsStringIgnoringCase(...\func_get_args()); +} + +/** + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotContainsString + */ +function assertStringNotContainsString(string $needle, string $haystack, string $message = ''): void +{ + Assert::assertStringNotContainsString(...\func_get_args()); +} + +/** + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringNotContainsStringIgnoringCase + */ +function assertStringNotContainsStringIgnoringCase(string $needle, string $haystack, string $message = ''): void +{ + Assert::assertStringNotContainsStringIgnoringCase(...\func_get_args()); +} + +/** + * Asserts that a string ends with a given suffix. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringEndsWith + */ +function assertStringEndsWith(string $suffix, string $string, string $message = ''): void +{ + Assert::assertStringEndsWith(...\func_get_args()); +} + +/** + * Asserts that a string ends not with a given suffix. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertStringEndsNotWith + */ +function assertStringEndsNotWith(string $suffix, string $string, string $message = ''): void +{ + Assert::assertStringEndsNotWith(...\func_get_args()); +} + +/** + * Asserts that two XML files are equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertXmlFileEqualsXmlFile + */ +function assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile, string $message = ''): void +{ + Assert::assertXmlFileEqualsXmlFile(...\func_get_args()); +} + +/** + * Asserts that two XML files are not equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertXmlFileNotEqualsXmlFile + */ +function assertXmlFileNotEqualsXmlFile(string $expectedFile, string $actualFile, string $message = ''): void +{ + Assert::assertXmlFileNotEqualsXmlFile(...\func_get_args()); +} + +/** + * Asserts that two XML documents are equal. + * + * @param \DOMDocument|string $actualXml + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertXmlStringEqualsXmlFile + */ +function assertXmlStringEqualsXmlFile(string $expectedFile, $actualXml, string $message = ''): void +{ + Assert::assertXmlStringEqualsXmlFile(...\func_get_args()); +} + +/** + * Asserts that two XML documents are not equal. + * + * @param \DOMDocument|string $actualXml + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertXmlStringNotEqualsXmlFile + */ +function assertXmlStringNotEqualsXmlFile(string $expectedFile, $actualXml, string $message = ''): void +{ + Assert::assertXmlStringNotEqualsXmlFile(...\func_get_args()); +} + +/** + * Asserts that two XML documents are equal. + * + * @param \DOMDocument|string $expectedXml + * @param \DOMDocument|string $actualXml + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertXmlStringEqualsXmlString + */ +function assertXmlStringEqualsXmlString($expectedXml, $actualXml, string $message = ''): void +{ + Assert::assertXmlStringEqualsXmlString(...\func_get_args()); +} + +/** + * Asserts that two XML documents are not equal. + * + * @param \DOMDocument|string $expectedXml + * @param \DOMDocument|string $actualXml + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * @throws Exception + * + * @see Assert::assertXmlStringNotEqualsXmlString + */ +function assertXmlStringNotEqualsXmlString($expectedXml, $actualXml, string $message = ''): void +{ + Assert::assertXmlStringNotEqualsXmlString(...\func_get_args()); +} + +/** + * Asserts that a hierarchy of DOMElements matches. + * + * @throws AssertionFailedError + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @codeCoverageIgnore + * + * @deprecated https://github.com/sebastianbergmann/phpunit/issues/4091 + * @see Assert::assertEqualXMLStructure + */ +function assertEqualXMLStructure(\DOMElement $expectedElement, \DOMElement $actualElement, bool $checkAttributes = false, string $message = ''): void +{ + Assert::assertEqualXMLStructure(...\func_get_args()); +} + +/** + * Evaluates a PHPUnit\Framework\Constraint matcher object. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertThat + */ +function assertThat($value, Constraint $constraint, string $message = ''): void +{ + Assert::assertThat(...\func_get_args()); +} + +/** + * Asserts that a string is a valid JSON string. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJson + */ +function assertJson(string $actualJson, string $message = ''): void +{ + Assert::assertJson(...\func_get_args()); +} + +/** + * Asserts that two given JSON encoded objects or arrays are equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJsonStringEqualsJsonString + */ +function assertJsonStringEqualsJsonString(string $expectedJson, string $actualJson, string $message = ''): void +{ + Assert::assertJsonStringEqualsJsonString(...\func_get_args()); +} + +/** + * Asserts that two given JSON encoded objects or arrays are not equal. + * + * @param string $expectedJson + * @param string $actualJson + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJsonStringNotEqualsJsonString + */ +function assertJsonStringNotEqualsJsonString($expectedJson, $actualJson, string $message = ''): void +{ + Assert::assertJsonStringNotEqualsJsonString(...\func_get_args()); +} + +/** + * Asserts that the generated JSON encoded object and the content of the given file are equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJsonStringEqualsJsonFile + */ +function assertJsonStringEqualsJsonFile(string $expectedFile, string $actualJson, string $message = ''): void +{ + Assert::assertJsonStringEqualsJsonFile(...\func_get_args()); +} + +/** + * Asserts that the generated JSON encoded object and the content of the given file are not equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJsonStringNotEqualsJsonFile + */ +function assertJsonStringNotEqualsJsonFile(string $expectedFile, string $actualJson, string $message = ''): void +{ + Assert::assertJsonStringNotEqualsJsonFile(...\func_get_args()); +} + +/** + * Asserts that two JSON files are equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJsonFileEqualsJsonFile + */ +function assertJsonFileEqualsJsonFile(string $expectedFile, string $actualFile, string $message = ''): void +{ + Assert::assertJsonFileEqualsJsonFile(...\func_get_args()); +} + +/** + * Asserts that two JSON files are not equal. + * + * @throws ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + * + * @see Assert::assertJsonFileNotEqualsJsonFile + */ +function assertJsonFileNotEqualsJsonFile(string $expectedFile, string $actualFile, string $message = ''): void +{ + Assert::assertJsonFileNotEqualsJsonFile(...\func_get_args()); +} diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..cc08702cb --- /dev/null +++ b/composer.json @@ -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" + ] + } + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..759697e08 --- /dev/null +++ b/phpstan.neon @@ -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.#" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 000000000..368f6bdda --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,16 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/rector.yaml b/rector.yaml new file mode 100644 index 000000000..90dd81168 --- /dev/null +++ b/rector.yaml @@ -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' diff --git a/scripts/compile.php b/scripts/compile.php new file mode 100644 index 000000000..56c3974c6 --- /dev/null +++ b/scripts/compile.php @@ -0,0 +1,36 @@ + + */ + private const OPTIONS = [self::COVERAGE_OPTION, self::MIN_OPTION]; + + /** + * If any, adds the coverage params to the given original arguments. + * + * @param array $originals + * + * @return array + */ + 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; + } +} diff --git a/src/Actions/AddsDefaults.php b/src/Actions/AddsDefaults.php new file mode 100644 index 000000000..951a1e46f --- /dev/null +++ b/src/Actions/AddsDefaults.php @@ -0,0 +1,29 @@ + $arguments + * + * @return array + */ + public static function to(array $arguments): array + { + if (!array_key_exists('printer', $arguments)) { + $arguments['printer'] = new Printer(); + } + + return $arguments; + } +} diff --git a/src/Actions/AddsTests.php b/src/Actions/AddsTests.php new file mode 100644 index 000000000..173d877c4 --- /dev/null +++ b/src/Actions/AddsTests.php @@ -0,0 +1,66 @@ + $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); + } +} diff --git a/src/Actions/LoadStructure.php b/src/Actions/LoadStructure.php new file mode 100644 index 000000000..35f816cf3 --- /dev/null +++ b/src/Actions/LoadStructure.php @@ -0,0 +1,61 @@ + + */ + 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); + } + } + } +} diff --git a/src/Actions/ValidatesConfiguration.php b/src/Actions/ValidatesConfiguration.php new file mode 100644 index 000000000..a2a6a481e --- /dev/null +++ b/src/Actions/ValidatesConfiguration.php @@ -0,0 +1,41 @@ + $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'); + } + } +} diff --git a/src/Actions/ValidatesEnvironment.php b/src/Actions/ValidatesEnvironment.php new file mode 100644 index 000000000..0f79d9029 --- /dev/null +++ b/src/Actions/ValidatesEnvironment.php @@ -0,0 +1,42 @@ + + */ + 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); + } + } + } +} diff --git a/src/Concerns/TestCase.php b/src/Concerns/TestCase.php new file mode 100644 index 000000000..7667ce86e --- /dev/null +++ b/src/Concerns/TestCase.php @@ -0,0 +1,145 @@ +__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\\'); + } +} diff --git a/src/Console/Command.php b/src/Console/Command.php new file mode 100644 index 000000000..7d4b84196 --- /dev/null +++ b/src/Console/Command.php @@ -0,0 +1,143 @@ +testSuite = $testSuite; + $this->output = $output; + } + + /** + * {@inheritdoc} + * + * @phpstan-ignore-next-line + * + * @param array $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 $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 FAIL Code coverage below expected: %s %%. Minimum: %s %%.", + number_format($coverage, 1), + number_format($this->testSuite->coverageMin, 1) + )); + } + } + + exit($result); + } +} diff --git a/src/Console/Coverage.php b/src/Console/Coverage.php new file mode 100644 index 000000000..2da149a0c --- /dev/null +++ b/src/Console/Coverage.php @@ -0,0 +1,167 @@ +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( + ' Cov: %s', + $totalCoverage + ) + ); + + $output->writeln(''); + + /** @var 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(' %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( + '%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 + */ + 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; + } +} diff --git a/src/Contracts/HasPrintableTestCaseName.php b/src/Contracts/HasPrintableTestCaseName.php new file mode 100644 index 000000000..0d95d15d7 --- /dev/null +++ b/src/Contracts/HasPrintableTestCaseName.php @@ -0,0 +1,21 @@ +> + */ + private static $datasets = []; + + /** + * Sets the given. + * + * @param Closure|iterable $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 + */ + 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|Closure|iterable|string|null $data + * + * @return array + */ + 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 $data + */ + private static function getDataSetDescription(array $data): string + { + $exporter = new Exporter(); + + return \sprintf(' with (%s)', $exporter->shortenedRecursiveExport($data)); + } +} diff --git a/src/Exceptions/AfterAllAlreadyExist.php b/src/Exceptions/AfterAllAlreadyExist.php new file mode 100644 index 000000000..b21de8f93 --- /dev/null +++ b/src/Exceptions/AfterAllAlreadyExist.php @@ -0,0 +1,24 @@ +getMessage(); + + parent::__construct(sprintf(<<|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 + */ + 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 + */ + 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; + } +} diff --git a/src/Laravel/Commands/PestDatasetCommand.php b/src/Laravel/Commands/PestDatasetCommand.php new file mode 100644 index 000000000..703f62645 --- /dev/null +++ b/src/Laravel/Commands/PestDatasetCommand.php @@ -0,0 +1,67 @@ +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)); + } +} diff --git a/src/Laravel/Commands/PestInstallCommand.php b/src/Laravel/Commands/PestInstallCommand.php new file mode 100644 index 000000000..fe4facfcb --- /dev/null +++ b/src/Laravel/Commands/PestInstallCommand.php @@ -0,0 +1,50 @@ +output->success('`tests/Pest.php` created successfully.'); + } +} diff --git a/src/Laravel/Commands/PestTestCommand.php b/src/Laravel/Commands/PestTestCommand.php new file mode 100644 index 000000000..28c474d30 --- /dev/null +++ b/src/Laravel/Commands/PestTestCommand.php @@ -0,0 +1,70 @@ +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)); + } +} diff --git a/src/Laravel/PestServiceProvider.php b/src/Laravel/PestServiceProvider.php new file mode 100644 index 000000000..5ac059e10 --- /dev/null +++ b/src/Laravel/PestServiceProvider.php @@ -0,0 +1,27 @@ +app->runningInConsole()) { + $this->commands([ + PestInstallCommand::class, + PestTestCommand::class, + PestDatasetCommand::class, + ]); + } + } +} diff --git a/src/PendingObjects/AfterEachCall.php b/src/PendingObjects/AfterEachCall.php new file mode 100644 index 000000000..0f021f827 --- /dev/null +++ b/src/PendingObjects/AfterEachCall.php @@ -0,0 +1,86 @@ +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 $arguments + */ + public function __call(string $name, array $arguments): self + { + $this->proxies + ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); + + return $this; + } +} diff --git a/src/PendingObjects/BeforeEachCall.php b/src/PendingObjects/BeforeEachCall.php new file mode 100644 index 000000000..b5df9cba2 --- /dev/null +++ b/src/PendingObjects/BeforeEachCall.php @@ -0,0 +1,86 @@ +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 $arguments + */ + public function __call(string $name, array $arguments): self + { + $this->proxies + ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); + + return $this; + } +} diff --git a/src/PendingObjects/TestCall.php b/src/PendingObjects/TestCall.php new file mode 100644 index 000000000..b25ec5372 --- /dev/null +++ b/src/PendingObjects/TestCall.php @@ -0,0 +1,133 @@ +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|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 $arguments + */ + public function __call(string $name, array $arguments): self + { + $this->testCaseFactory + ->chains + ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); + + return $this; + } +} diff --git a/src/PendingObjects/UsesCall.php b/src/PendingObjects/UsesCall.php new file mode 100644 index 000000000..d7cfc1f62 --- /dev/null +++ b/src/PendingObjects/UsesCall.php @@ -0,0 +1,97 @@ + + */ + private $classAndTraits; + + /** + * Holds the base dirname here the uses call was performed. + * + * @var string + */ + private $filename; + + /** + * Holds the targets of the uses. + * + * @var array + */ + private $targets; + + /** + * Holds the groups of the uses. + * + * @var array + */ + private $groups = []; + + /** + * Creates a new instance of a pending test uses. + * + * @param array $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); + } +} diff --git a/src/Repositories/AfterAllRepository.php b/src/Repositories/AfterAllRepository.php new file mode 100644 index 000000000..e0666da16 --- /dev/null +++ b/src/Repositories/AfterAllRepository.php @@ -0,0 +1,53 @@ + + */ + 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(); + } +} diff --git a/src/Repositories/AfterEachRepository.php b/src/Repositories/AfterEachRepository.php new file mode 100644 index 000000000..fd7f0bf07 --- /dev/null +++ b/src/Repositories/AfterEachRepository.php @@ -0,0 +1,48 @@ + + */ + 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); + } +} diff --git a/src/Repositories/BeforeAllRepository.php b/src/Repositories/BeforeAllRepository.php new file mode 100644 index 000000000..fa5b0b801 --- /dev/null +++ b/src/Repositories/BeforeAllRepository.php @@ -0,0 +1,55 @@ + + */ + 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(); + } +} diff --git a/src/Repositories/BeforeEachRepository.php b/src/Repositories/BeforeEachRepository.php new file mode 100644 index 000000000..f9bbb1adb --- /dev/null +++ b/src/Repositories/BeforeEachRepository.php @@ -0,0 +1,40 @@ + + */ + 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(); + } +} diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php new file mode 100644 index 000000000..2aefe320e --- /dev/null +++ b/src/Repositories/TestRepository.php @@ -0,0 +1,122 @@ + + */ + private $state = []; + + /** + * @var array>> + */ + 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 $classOrTraits + * @param array $groups + * @param array $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; + } +} diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php new file mode 100644 index 000000000..e21998960 --- /dev/null +++ b/src/Support/Backtrace.php @@ -0,0 +1,35 @@ +getMessage(), self::UNDEFINED_METHOD)) { + $message = str_replace(self::UNDEFINED_METHOD, 'Call to undefined method ', $message); + + Reflection::setPropertyValue($throwable, 'message', $message); + } + + throw $throwable; + } + } +} diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php new file mode 100644 index 000000000..53b59c613 --- /dev/null +++ b/src/Support/HigherOrderMessage.php @@ -0,0 +1,60 @@ + + * + * @readonly + */ + public $arguments; + + /** + * Creates a new higher order message. + * + * @param array $arguments + */ + public function __construct(string $filename, int $line, string $methodName, array $arguments) + { + $this->filename = $filename; + $this->line = $line; + $this->methodName = $methodName; + $this->arguments = $arguments; + } +} diff --git a/src/Support/HigherOrderMessageCollection.php b/src/Support/HigherOrderMessageCollection.php new file mode 100644 index 000000000..fa52308af --- /dev/null +++ b/src/Support/HigherOrderMessageCollection.php @@ -0,0 +1,74 @@ + + */ + private $messages = []; + + /** + * Adds a new higher order message to the collection. + * + * @param array $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; + } + } +} diff --git a/src/Support/NullClosure.php b/src/Support/NullClosure.php new file mode 100644 index 000000000..10d9c335a --- /dev/null +++ b/src/Support/NullClosure.php @@ -0,0 +1,22 @@ + $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); + } +} diff --git a/src/Support/Str.php b/src/Support/Str.php new file mode 100644 index 000000000..aa1356476 --- /dev/null +++ b/src/Support/Str.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/src/globals.php b/src/globals.php new file mode 100644 index 000000000..4a6aaac97 --- /dev/null +++ b/src/globals.php @@ -0,0 +1,101 @@ +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); +} diff --git a/stubs/Dataset.php b/stubs/Dataset.php new file mode 100644 index 000000000..fc2d634ff --- /dev/null +++ b/stubs/Dataset.php @@ -0,0 +1,5 @@ +get('/{name}'); + + $response->assertStatus(200); +}); diff --git a/stubs/Pest.php b/stubs/Pest.php new file mode 100644 index 000000000..a7b28ca85 --- /dev/null +++ b/stubs/Pest.php @@ -0,0 +1,3 @@ +in('Feature'); diff --git a/stubs/Unit.php b/stubs/Unit.php new file mode 100644 index 000000000..0f429e880 --- /dev/null +++ b/stubs/Unit.php @@ -0,0 +1,5 @@ + + + + + ./tests/Unit + + + ./tests/Feature + + + + + ./app + ./src + + + diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt new file mode 100644 index 000000000..4b544dc79 --- /dev/null +++ b/tests/.snapshots/success.txt @@ -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 diff --git a/tests/Autoload.php b/tests/Autoload.php new file mode 100644 index 000000000..e796a6497 --- /dev/null +++ b/tests/Autoload.php @@ -0,0 +1,5 @@ +register(); +} diff --git a/tests/Datasets/Numbers.php b/tests/Datasets/Numbers.php new file mode 100644 index 000000000..2b718699e --- /dev/null +++ b/tests/Datasets/Numbers.php @@ -0,0 +1,15 @@ +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); +}); diff --git a/tests/Features/BeforeAll.php b/tests/Features/BeforeAll.php new file mode 100644 index 000000000..4095e68af --- /dev/null +++ b/tests/Features/BeforeAll.php @@ -0,0 +1,18 @@ +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'); +}); diff --git a/tests/Features/BeforeEach.php b/tests/Features/BeforeEach.php new file mode 100644 index 000000000..94c5adc39 --- /dev/null +++ b/tests/Features/BeforeEach.php @@ -0,0 +1,15 @@ +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); +}); diff --git a/tests/Features/Datasets.php b/tests/Features/Datasets.php new file mode 100644 index 000000000..688616c09 --- /dev/null +++ b/tests/Features/Datasets.php @@ -0,0 +1,108 @@ +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); diff --git a/tests/Features/Exceptions.php b/tests/Features/Exceptions.php new file mode 100644 index 000000000..5e7e51a97 --- /dev/null +++ b/tests/Features/Exceptions.php @@ -0,0 +1,15 @@ +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'); diff --git a/tests/Features/HigherOrderMessages.php b/tests/Features/HigherOrderMessages.php new file mode 100644 index 000000000..ca48ed4cd --- /dev/null +++ b/tests/Features/HigherOrderMessages.php @@ -0,0 +1,7 @@ +assertTrue(true); + +it('proxies calls to object')->assertTrue(true); + +afterEach()->assertTrue(true); diff --git a/tests/Features/It.php b/tests/Features/It.php new file mode 100644 index 000000000..9ab6e3847 --- /dev/null +++ b/tests/Features/It.php @@ -0,0 +1,7 @@ + 'foo']); +}); + +it('is a higher order message test')->assertTrue(true); diff --git a/tests/Features/Mocks.php b/tests/Features/Mocks.php new file mode 100644 index 000000000..1760f0570 --- /dev/null +++ b/tests/Features/Mocks.php @@ -0,0 +1,15 @@ +shouldReceive('bar') + ->times(1) + ->andReturn(2); + + assertEquals(2, $mock->bar()); +}); diff --git a/tests/Features/Skip.php b/tests/Features/Skip.php new file mode 100644 index 000000000..99d6f0a84 --- /dev/null +++ b/tests/Features/Skip.php @@ -0,0 +1,29 @@ +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); diff --git a/tests/Features/Test.php b/tests/Features/Test.php new file mode 100644 index 000000000..d2e53ed03 --- /dev/null +++ b/tests/Features/Test.php @@ -0,0 +1,7 @@ + 'foo']); +}); + +test('higher order message test')->assertTrue(true); diff --git a/tests/Features/TestCycle.php b/tests/Features/TestCycle.php new file mode 100644 index 000000000..d941dcf28 --- /dev/null +++ b/tests/Features/TestCycle.php @@ -0,0 +1,27 @@ +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); +}); diff --git a/tests/Fixtures/DirectoryWithTests/ExampleTest.php b/tests/Fixtures/DirectoryWithTests/ExampleTest.php new file mode 100644 index 000000000..8553ad34b --- /dev/null +++ b/tests/Fixtures/DirectoryWithTests/ExampleTest.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/Fixtures/ExampleTest.php b/tests/Fixtures/ExampleTest.php new file mode 100644 index 000000000..8553ad34b --- /dev/null +++ b/tests/Fixtures/ExampleTest.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/Fixtures/phpunit-in-isolation.xml b/tests/Fixtures/phpunit-in-isolation.xml new file mode 100644 index 000000000..28fa86cb1 --- /dev/null +++ b/tests/Fixtures/phpunit-in-isolation.xml @@ -0,0 +1,8 @@ + + + diff --git a/tests/Fixtures/phpunit-not-in-isolation.xml b/tests/Fixtures/phpunit-not-in-isolation.xml new file mode 100644 index 000000000..898e6e430 --- /dev/null +++ b/tests/Fixtures/phpunit-not-in-isolation.xml @@ -0,0 +1,8 @@ + + + diff --git a/tests/PHPUnit/CustomTestCase/CustomTestCase.php b/tests/PHPUnit/CustomTestCase/CustomTestCase.php new file mode 100644 index 000000000..2eb0ad4e6 --- /dev/null +++ b/tests/PHPUnit/CustomTestCase/CustomTestCase.php @@ -0,0 +1,16 @@ +assertTrue(true); + } +} + +// register_shutdown_function(fn () => assertTrue(PhpunitTest::$executed)); diff --git a/tests/PHPUnit/CustomTestCase/UsesPerDirectory.php b/tests/PHPUnit/CustomTestCase/UsesPerDirectory.php new file mode 100644 index 000000000..ce947929a --- /dev/null +++ b/tests/PHPUnit/CustomTestCase/UsesPerDirectory.php @@ -0,0 +1,7 @@ +in(__DIR__); + +test('closure was bound to CustomTestCase', function () { + $this->assertCustomTrue(); +}); diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php new file mode 100644 index 000000000..2cd2999dc --- /dev/null +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder/SubFolder/CustomTestCaseInSubFolder.php @@ -0,0 +1,15 @@ +assertCustomInSubFolderTrue(); +}); diff --git a/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php new file mode 100644 index 000000000..e06731749 --- /dev/null +++ b/tests/PHPUnit/CustomTestCaseInSubFolders/SubFolder2/UsesPerFile.php @@ -0,0 +1,25 @@ +assertTrueIsTrue(); +}); + +test('trait applied in this file')->assertTrueIsTrue(); diff --git a/tests/PHPUnit/Pest.php b/tests/PHPUnit/Pest.php new file mode 100644 index 000000000..0a8862659 --- /dev/null +++ b/tests/PHPUnit/Pest.php @@ -0,0 +1,3 @@ +in('CustomTestCaseInSubFolders/SubFolder'); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 000000000..2090100f0 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,3 @@ +group('integration')->in('Visual'); diff --git a/tests/Playground.php b/tests/Playground.php new file mode 100644 index 000000000..e24277b1e --- /dev/null +++ b/tests/Playground.php @@ -0,0 +1,5 @@ +coverage); + + $arguments = AddsCoverage::from($testSuite, []); + assertEquals([], $arguments); + assertFalse($testSuite->coverage); + + $arguments = AddsCoverage::from($testSuite, ['--coverage']); + assertEquals(['--coverage-php', \Pest\Console\Coverage::getPath()], $arguments); + assertTrue($testSuite->coverage); +}); + +it('adds coverage if --min exist', function () { + $testSuite = new TestSuite(getcwd()); + assertEquals($testSuite->coverageMin, 0.0); + + assertFalse($testSuite->coverage); + AddsCoverage::from($testSuite, []); + assertEquals($testSuite->coverageMin, 0.0); + + AddsCoverage::from($testSuite, ['--min=2']); + assertEquals($testSuite->coverageMin, 2.0); + + AddsCoverage::from($testSuite, ['--min=2.4']); + assertEquals($testSuite->coverageMin, 2.4); +}); diff --git a/tests/Unit/Actions/AddsDefaults.php b/tests/Unit/Actions/AddsDefaults.php new file mode 100644 index 000000000..7a55adc5e --- /dev/null +++ b/tests/Unit/Actions/AddsDefaults.php @@ -0,0 +1,20 @@ + 'foo']); + + assertInstanceOf(Printer::class, $arguments['printer']); + assertEquals($arguments['bar'], 'foo'); +}); + +it('does not override options', function () { + $defaultResultPrinter = new DefaultResultPrinter(); + + assertEquals(AddsDefaults::to(['printer' => $defaultResultPrinter]), [ + 'printer' => $defaultResultPrinter, + ]); +}); diff --git a/tests/Unit/Actions/AddsTests.php b/tests/Unit/Actions/AddsTests.php new file mode 100644 index 000000000..925e35746 --- /dev/null +++ b/tests/Unit/Actions/AddsTests.php @@ -0,0 +1,32 @@ +addTest($phpUnitTestCase); + assertCount(1, $testSuite->tests()); + + AddsTests::to($testSuite, new \Pest\TestSuite(getcwd())); + assertCount(1, $testSuite->tests()); +}); + +it('removes warnings', function () use ($pestTestCase) { + $testSuite = new TestSuite(); + $warningTestCase = new WarningTestCase('No tests found in class "Pest\TestCase".'); + $testSuite->addTest($warningTestCase); + + AddsTests::to($testSuite, new \Pest\TestSuite(getcwd())); + assertCount(0, $testSuite->tests()); +}); diff --git a/tests/Unit/Actions/ValidatesConfiguration.php b/tests/Unit/Actions/ValidatesConfiguration.php new file mode 100644 index 000000000..24edc4f09 --- /dev/null +++ b/tests/Unit/Actions/ValidatesConfiguration.php @@ -0,0 +1,42 @@ +expectException(FileOrFolderNotFound::class); + + ValidatesConfiguration::in([ + 'configuration' => 'foo', + ]); +}); + +it('throws exception when `process isolation` is true', function () { + $this->expectException(AttributeNotSupportedYet::class); + $this->expectExceptionMessage('The PHPUnit attribute `processIsolation` with value `true` is not supported yet.'); + + $filename = implode(DIRECTORY_SEPARATOR, [ + dirname(__DIR__, 2), + 'Fixtures', + 'phpunit-in-isolation.xml', + ]); + + ValidatesConfiguration::in([ + 'configuration' => $filename, + ]); +}); + +it('do not throws exception when `process isolation` is false', function () { + $filename = implode(DIRECTORY_SEPARATOR, [ + dirname(__DIR__, 2), + 'Fixtures', + 'phpunit-not-in-isolation.xml', + ]); + + ValidatesConfiguration::in([ + 'configuration' => $filename, + ]); + + assertTrue(true); +}); diff --git a/tests/Unit/Console/Coverage.php b/tests/Unit/Console/Coverage.php new file mode 100644 index 000000000..1a395039a --- /dev/null +++ b/tests/Unit/Console/Coverage.php @@ -0,0 +1,24 @@ + ['foo'], + 2 => ['bar'], + 4 => [], + 5 => [], + 6 => [], + 7 => null, + 100 => null, + 101 => ['foo'], + 102 => [], + ]; + } + })); +}); diff --git a/tests/Unit/Support/Backtrace.php b/tests/Unit/Support/Backtrace.php new file mode 100644 index 000000000..558865126 --- /dev/null +++ b/tests/Unit/Support/Backtrace.php @@ -0,0 +1,11 @@ +tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); + $this->expectException(TestAlreadyExist::class); + $this->expectExceptionMessage(sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__)); + $testSuite->tests->set(new \Pest\Factories\TestCaseFactory(__FILE__, 'foo', $test)); +}); diff --git a/tests/Visual/SingleTestOrDirectory.php b/tests/Visual/SingleTestOrDirectory.php new file mode 100644 index 000000000..6dd9204bd --- /dev/null +++ b/tests/Visual/SingleTestOrDirectory.php @@ -0,0 +1,32 @@ +run(); + + return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); +}; + +test('allows to run a single test', function () use ($run) { + assertStringContainsString(<< 'integration', 'REBUILD_SNAPSHOTS' => false])); + + $process->run(); + + return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + }; + + if (getenv('REBUILD_SNAPSHOTS')) { + file_put_contents($snapshot, $output()); + } elseif (!getenv('EXCLUDE')) { + $output = explode("\n", $output()); + array_pop($output); + array_pop($output); + assertStringContainsString(implode("\n", $output), file_get_contents($snapshot)); + } +})->skip(!getenv('REBUILD_SNAPSHOTS') && getenv('EXCLUDE'));