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** 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'));