mirror of
https://github.com/pestphp/pest.git
synced 2026-03-07 00:07:22 +01:00
merge from master
This commit is contained in:
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@ -7,8 +7,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
php: ['7.3', '7.4', '8.0', '8.1']
|
||||
os: [ubuntu-latest] # (macos-latest, windows-latest) 2.x-dev is under development
|
||||
php: ['8.0', '8.1']
|
||||
dependency-version: [prefer-lowest, prefer-stable]
|
||||
parallel: ['', '--parallel']
|
||||
exclude:
|
||||
@ -38,8 +38,13 @@ jobs:
|
||||
- name: Install PHP dependencies
|
||||
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress
|
||||
|
||||
- name: Unit Tests
|
||||
run: php bin/pest --colors=always --exclude-group=integration
|
||||
|
||||
- name: Unit Tests
|
||||
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
|
||||
if: ${{ false }} # 2.x-dev is under development
|
||||
|
||||
- name: Integration Tests
|
||||
run: php bin/pest --colors=always --group=integration
|
||||
if: ${{ false }} # 2.x-dev is under development
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'overrides')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
|
||||
->append(['.php-cs-fixer.dist.php']);
|
||||
|
||||
18
README.md
18
README.md
@ -19,14 +19,18 @@
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Pest development. If you are interested in becoming a sponsor, please visit the Nuno Maduro's [Sponsors page](https://github.com/sponsors/nunomaduro).
|
||||
|
||||
### Platinum Sponsors
|
||||
|
||||
- **[Spatie](https://spatie.be)**
|
||||
- **[Worksome](https://www.worksome.com/)**
|
||||
|
||||
### Premium Sponsors
|
||||
|
||||
- **[Akaunting](https://akaunting.com)**
|
||||
- **[Auth0](https://auth0.com)**
|
||||
- **[Codecourse](https://codecourse.com/)**
|
||||
- **[Fathom Analytics](https://usefathom.com/)**
|
||||
- **[Meema](https://meema.io)**
|
||||
- **[Scout APM](https://scoutapm.com)**
|
||||
- **[Spatie](https://spatie.be)**
|
||||
- [Akaunting](https://akaunting.com)
|
||||
- [Auth0](https://auth0.com)
|
||||
- [Codecourse](https://codecourse.com/)
|
||||
- [Fathom Analytics](https://usefathom.com/)
|
||||
- [Meema](https://meema.io)
|
||||
- [Scout APM](https://scoutapm.com)
|
||||
|
||||
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
||||
|
||||
4
TODO.md
Normal file
4
TODO.md
Normal file
@ -0,0 +1,4 @@
|
||||
1. Support for `--help` pest options.
|
||||
2. Support for `default` printer.
|
||||
3. Support for `TeamCity` printer.
|
||||
4. Support for `JUnit` log.
|
||||
19
bin/pest
19
bin/pest
@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use NunoMaduro\Collision\Provider;
|
||||
use Pest\Actions\ValidatesEnvironment;
|
||||
use Pest\Support\Container;
|
||||
use Pest\Kernel;
|
||||
use Pest\TestSuite;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@ -25,8 +24,6 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
$autoloadPath = $localPath;
|
||||
}
|
||||
|
||||
(new Provider())->register();
|
||||
|
||||
// Get $rootPath based on $autoloadPath
|
||||
$rootPath = dirname($autoloadPath, 2);
|
||||
$argv = new ArgvInput();
|
||||
@ -40,8 +37,6 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
$container->add(TestSuite::class, $testSuite);
|
||||
$container->add(OutputInterface::class, $output);
|
||||
|
||||
ValidatesEnvironment::in($testSuite);
|
||||
|
||||
$args = $_SERVER['argv'];
|
||||
|
||||
// Let's remove any arguments that PHPUnit does not understand
|
||||
@ -53,11 +48,11 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
}
|
||||
}
|
||||
|
||||
if (($runInParallel = $argv->hasParameterOption(['--parallel', '-p'])) && !class_exists(\Pest\Parallel\Command::class)) {
|
||||
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
|
||||
exit(Command::FAILURE);
|
||||
}
|
||||
$kernel = Kernel::boot();
|
||||
|
||||
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class;
|
||||
exit($container->get($command)->run($args));
|
||||
$result = $kernel->handle($args);
|
||||
|
||||
$kernel->shutdown();
|
||||
|
||||
exit($result);
|
||||
})();
|
||||
|
||||
@ -17,16 +17,21 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.3 || ^8.0",
|
||||
"nunomaduro/collision": "^5.4.0|^6.0",
|
||||
"php": "^8.0",
|
||||
"nunomaduro/collision": "^5.10.0|^6.0",
|
||||
"pestphp/pest-plugin": "^1.0.0",
|
||||
"phpunit/phpunit": "^9.5.5"
|
||||
"phpunit/phpunit": "10.0.x-dev"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Pest\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"../phpunit/src/Runner/TestSuiteLoader.php",
|
||||
"vendor/phpunit/phpunit/src/Runner/TestSuiteLoader.php"
|
||||
],
|
||||
"files": [
|
||||
"overrides/Runner/TestSuiteLoader.php",
|
||||
"src/Functions.php",
|
||||
"src/Pest.php"
|
||||
]
|
||||
@ -43,8 +48,7 @@
|
||||
"illuminate/console": "^8.47.0",
|
||||
"illuminate/support": "^8.47.0",
|
||||
"laravel/dusk": "^6.15.0",
|
||||
"pestphp/pest-dev-tools": "dev-master",
|
||||
"pestphp/pest-plugin-parallel": "^1.0"
|
||||
"pestphp/pest-dev-tools": "dev-master"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
@ -58,22 +62,20 @@
|
||||
"scripts": {
|
||||
"lint": "php-cs-fixer fix -v",
|
||||
"test:lint": "php-cs-fixer fix -v --dry-run",
|
||||
"test:types": "phpstan analyse --ansi --memory-limit=-1",
|
||||
"test:types": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
|
||||
"test:parallel": "php bin/pest -p --colors=always --exclude-group=integration",
|
||||
"test:integration": "php bin/pest --colors=always --group=integration",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
|
||||
"test:parallel": "exit 1",
|
||||
"test:integration": "exit 1",
|
||||
"update:snapshots": "exit 1",
|
||||
"test": [
|
||||
"@test:lint",
|
||||
"@test:types",
|
||||
"@test:unit",
|
||||
"@test:parallel",
|
||||
"@test:integration"
|
||||
"@test:unit"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.x-dev"
|
||||
"dev-next": "2.x-dev"
|
||||
},
|
||||
"pest": {
|
||||
"plugins": [
|
||||
|
||||
175
overrides/Runner/TestSuiteLoader.php
Normal file
175
overrides/Runner/TestSuiteLoader.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright (c) 2001-2021, Sebastian Bergmann <sebastian@phpunit.de>.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the
|
||||
* distribution.
|
||||
*
|
||||
* * Neither the name of Sebastian Bergmann nor the names of his
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PHPUnit\Runner;
|
||||
|
||||
use function array_diff;
|
||||
use function array_values;
|
||||
use function basename;
|
||||
use function class_exists;
|
||||
use function get_declared_classes;
|
||||
use Pest\IgnorableTestCase;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use function stripos;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class TestSuiteLoader
|
||||
{
|
||||
/**
|
||||
* @psalm-var list<class-string>
|
||||
*/
|
||||
private static array $loadedClasses = [];
|
||||
|
||||
/**
|
||||
* @psalm-var list<class-string>
|
||||
*/
|
||||
private static array $declaredClasses = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (empty(self::$declaredClasses)) {
|
||||
self::$declaredClasses = get_declared_classes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function load(string $suiteClassFile): ReflectionClass
|
||||
{
|
||||
$suiteClassName = $this->classNameFromFileName($suiteClassFile);
|
||||
|
||||
if (!class_exists($suiteClassName, false)) {
|
||||
(static function () use ($suiteClassFile) {
|
||||
include_once $suiteClassFile;
|
||||
|
||||
TestSuite::getInstance()->tests->makeIfExists($suiteClassFile);
|
||||
})();
|
||||
|
||||
$loadedClasses = array_values(
|
||||
array_diff(
|
||||
get_declared_classes(),
|
||||
array_merge(
|
||||
self::$declaredClasses,
|
||||
self::$loadedClasses
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses);
|
||||
|
||||
if (empty(self::$loadedClasses)) {
|
||||
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists($suiteClassName, false)) {
|
||||
// this block will handle namespaced classes
|
||||
$offset = 0 - strlen($suiteClassName);
|
||||
|
||||
foreach (self::$loadedClasses as $loadedClass) {
|
||||
if (stripos(substr($loadedClass, $offset - 1), '\\' . $suiteClassName) === 0) {
|
||||
$suiteClassName = $loadedClass;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists($suiteClassName, false)) {
|
||||
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||
}
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($suiteClassName);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ($class->isSubclassOf(TestCase::class) && !$class->isAbstract()) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
if ($class->hasMethod('suite')) {
|
||||
try {
|
||||
$method = $class->getMethod('suite');
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if (!$method->isAbstract() && $method->isPublic() && $method->isStatic()) {
|
||||
return $class;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||
}
|
||||
|
||||
public function reload(ReflectionClass $aClass): ReflectionClass
|
||||
{
|
||||
return $aClass;
|
||||
}
|
||||
|
||||
private function classNameFromFileName(string $suiteClassFile): string
|
||||
{
|
||||
$className = basename($suiteClassFile, '.php');
|
||||
$dotPos = strpos($className, '.');
|
||||
|
||||
if ($dotPos !== false) {
|
||||
$className = substr($className, 0, $dotPos);
|
||||
}
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
private function exceptionFor(string $className, string $filename): ReflectionClass
|
||||
{
|
||||
return new ReflectionClass(IgnorableTestCase::class);
|
||||
}
|
||||
}
|
||||
15
phpstan.neon
15
phpstan.neon
@ -4,7 +4,7 @@ includes:
|
||||
- vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
|
||||
|
||||
parameters:
|
||||
level: max
|
||||
level: 5
|
||||
paths:
|
||||
- src
|
||||
|
||||
@ -13,6 +13,7 @@ parameters:
|
||||
reportUnmatchedIgnoredErrors: true
|
||||
|
||||
ignoreErrors:
|
||||
- "#with a nullable type declaration#"
|
||||
- "#type mixed is not subtype of native#"
|
||||
- "#is not allowed to extend#"
|
||||
- "#Language construct eval#"
|
||||
@ -20,15 +21,3 @@ parameters:
|
||||
- "#has parameter \\$closure with default value.#"
|
||||
- "#has parameter \\$description with default value.#"
|
||||
- "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#"
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#invalid typehint type Pest\\Concerns\\Testable#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#is not subtype of native type PHPUnit\\Framework\\Test#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getPrintableTestCaseName\(\)#'
|
||||
path: src/Logging
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
|
||||
use NunoMaduro\Collision\Adapters\Phpunit\Printer;
|
||||
use Pest\Logging\JUnit;
|
||||
use Pest\Logging\TeamCity;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AddsDefaults
|
||||
{
|
||||
private const PRINTER = 'printer';
|
||||
|
||||
/**
|
||||
* Adds default arguments to the given `arguments` array.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function to(array $arguments): array
|
||||
{
|
||||
if (!array_key_exists(self::PRINTER, $arguments)) {
|
||||
$arguments[self::PRINTER] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
|
||||
}
|
||||
|
||||
if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) {
|
||||
$arguments[self::PRINTER] = new TeamCity(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
|
||||
}
|
||||
|
||||
// Load our junit logger instead.
|
||||
if (array_key_exists('junitLogfile', $arguments)) {
|
||||
$arguments['listeners'][] = new JUnit(
|
||||
$arguments['junitLogfile']
|
||||
);
|
||||
unset($arguments['junitLogfile']);
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\WarningTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AddsTests
|
||||
{
|
||||
/**
|
||||
* Adds tests to the given test suite.
|
||||
*
|
||||
* @param TestSuite<\PHPUnit\Framework\TestCase> $testSuite
|
||||
*/
|
||||
public static function to(TestSuite $testSuite, \Pest\TestSuite $pestTestSuite): void
|
||||
{
|
||||
self::removeTestClosureWarnings($testSuite);
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
|
||||
use Pest\Contracts\Plugins\AddsOutput;
|
||||
use Pest\Contracts\Plugins\HandlesArguments;
|
||||
use Pest\Plugin\Loader;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InteractsWithPlugins
|
||||
{
|
||||
/**
|
||||
* Transform the input arguments by passing it to the relevant plugins.
|
||||
*
|
||||
* @param array<int, string> $argv
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function handleArguments(array $argv): array
|
||||
{
|
||||
$plugins = Loader::getPlugins(HandlesArguments::class);
|
||||
|
||||
/** @var HandlesArguments $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$argv = $plugin->handleArguments($argv);
|
||||
}
|
||||
|
||||
return $argv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an opportunity for any plugins that want
|
||||
* to provide additional output after test execution.
|
||||
*/
|
||||
public static function addOutput(int $result): int
|
||||
{
|
||||
$plugins = Loader::getPlugins(AddsOutput::class);
|
||||
|
||||
/** @var AddsOutput $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$result = $plugin->addOutput($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
|
||||
use Pest\Exceptions\AttributeNotSupportedYet;
|
||||
use Pest\Exceptions\FileOrFolderNotFound;
|
||||
use PHPUnit\TextUI\XmlConfiguration\Loader;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ValidatesConfiguration
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private const CONFIGURATION_KEY = 'configuration';
|
||||
|
||||
/**
|
||||
* Validates the configuration in the given `configuration`.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function in($arguments): void
|
||||
{
|
||||
if (!array_key_exists(self::CONFIGURATION_KEY, $arguments) || !file_exists($arguments[self::CONFIGURATION_KEY])) {
|
||||
throw new FileOrFolderNotFound('phpunit.xml');
|
||||
}
|
||||
|
||||
$configuration = (new Loader())->load($arguments[self::CONFIGURATION_KEY])->phpunit();
|
||||
|
||||
if ($configuration->processIsolation()) {
|
||||
throw new AttributeNotSupportedYet('processIsolation', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
|
||||
use Pest\Exceptions\FileOrFolderNotFound;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ValidatesEnvironment
|
||||
{
|
||||
/**
|
||||
* The need files on the root path.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const NEEDED_FILES = [
|
||||
'composer.json',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Bootstrappers/BootExceptionHandler.php
Normal file
23
src/Bootstrappers/BootExceptionHandler.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Bootstrappers;
|
||||
|
||||
use NunoMaduro\Collision;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BootExceptionHandler
|
||||
{
|
||||
/**
|
||||
* Boots the Exception Handler.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
$handler = new Collision\Provider();
|
||||
|
||||
$handler->register();
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,18 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Actions;
|
||||
namespace Pest\Bootstrappers;
|
||||
|
||||
use Pest\Support\Str;
|
||||
use function Pest\testDirectory;
|
||||
use PHPUnit\Util\FileLoader;
|
||||
use Pest\TestSuite;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class LoadStructure
|
||||
final class BootFiles
|
||||
{
|
||||
/**
|
||||
* The Pest convention.
|
||||
@ -21,24 +21,23 @@ final class LoadStructure
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const STRUCTURE = [
|
||||
'Expectations.php',
|
||||
'Datasets',
|
||||
'Datasets.php',
|
||||
'Expectations',
|
||||
'Expectations.php',
|
||||
'Helpers',
|
||||
'Helpers.php',
|
||||
'Pest.php',
|
||||
'Datasets',
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates the configuration in the given `configuration`.
|
||||
* Boots the Subscribers.
|
||||
*/
|
||||
public static function in(string $rootPath): void
|
||||
public function __invoke(): void
|
||||
{
|
||||
$rootPath = TestSuite::getInstance()->rootPath;
|
||||
$testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
|
||||
|
||||
$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);
|
||||
|
||||
@ -50,14 +49,21 @@ final class LoadStructure
|
||||
$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;
|
||||
}
|
||||
$this->load($file->__toString());
|
||||
}
|
||||
} else {
|
||||
$load($filename);
|
||||
$this->load($filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given filename, if possible.
|
||||
*/
|
||||
private function load(string $filename): void
|
||||
{
|
||||
if (Str::endsWith($filename, '.php') && file_exists($filename)) {
|
||||
include_once $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Bootstrappers/BootSubscribers.php
Normal file
36
src/Bootstrappers/BootSubscribers.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Bootstrappers;
|
||||
|
||||
use Pest\Subscribers;
|
||||
use PHPUnit\Event;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BootSubscribers
|
||||
{
|
||||
/**
|
||||
* The Kernel subscribers.
|
||||
*
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
private static array $subscribers = [
|
||||
Subscribers\EnsureConfigurationIsValid::class,
|
||||
Subscribers\EnsureConfigurationDefaults::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Boots the Subscribers.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
foreach (self::$subscribers as $subscriber) {
|
||||
Event\Facade::registerSubscriber(
|
||||
new $subscriber()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,13 +14,13 @@ trait Expectable
|
||||
/**
|
||||
* @template TValue
|
||||
*
|
||||
* Creates a new expectation.
|
||||
* Creates a new Expectation.
|
||||
*
|
||||
* @param TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function expect($value): Expectation
|
||||
public function expect(mixed $value): Expectation
|
||||
{
|
||||
return new Expectation($value);
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ namespace Pest\Concerns;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Closure;
|
||||
use Pest\Expectation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -14,15 +13,17 @@ use Pest\Expectation;
|
||||
trait Extendable
|
||||
{
|
||||
/**
|
||||
* The list of extends.
|
||||
*
|
||||
* @var array<string, Closure>
|
||||
*/
|
||||
private static $extends = [];
|
||||
private static array $extends = [];
|
||||
|
||||
/** @var array<string, array<Closure>> */
|
||||
private static $pipes = [];
|
||||
private static array $pipes = [];
|
||||
|
||||
/**
|
||||
* Register a custom extend.
|
||||
* Register a new extend.
|
||||
*/
|
||||
public static function extend(string $name, Closure $extend): void
|
||||
{
|
||||
@ -30,7 +31,7 @@ trait Extendable
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a a pipe to be applied before an expectation is checked.
|
||||
* Register a pipe to be applied before an expectation is checked.
|
||||
*/
|
||||
public static function pipe(string $name, Closure $pipe): void
|
||||
{
|
||||
@ -39,10 +40,8 @@ trait Extendable
|
||||
|
||||
/**
|
||||
* Recister an interceptor that should replace an existing expectation.
|
||||
*
|
||||
* @param string|Closure $filter
|
||||
*/
|
||||
public static function intercept(string $name, $filter, Closure $handler): void
|
||||
public static function intercept(string $name, string|Closure $filter, Closure $handler): void
|
||||
{
|
||||
if (is_string($filter)) {
|
||||
$filter = function ($value) use ($filter): bool {
|
||||
@ -65,7 +64,7 @@ trait Extendable
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if extend is registered.
|
||||
* Checks if given extend name is registered.
|
||||
*/
|
||||
public static function hasExtend(string $name): bool
|
||||
{
|
||||
@ -102,10 +101,8 @@ trait Extendable
|
||||
* Dynamically handle calls to the class.
|
||||
*
|
||||
* @param array<int, mixed> $parameters
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $parameters)
|
||||
public function __call(string $method, array $parameters): mixed
|
||||
{
|
||||
if (!static::hasExtend($method)) {
|
||||
throw new BadMethodCallException("$method is not a callable method name.");
|
||||
|
||||
@ -9,21 +9,33 @@ namespace Pest\Concerns\Logging;
|
||||
*/
|
||||
trait WritesToConsole
|
||||
{
|
||||
/**
|
||||
* Writes the given success message to the console.
|
||||
*/
|
||||
private function writeSuccess(string $message): void
|
||||
{
|
||||
$this->writePestTestOutput($message, 'fg-green, bold', '✓');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given error message to the console.
|
||||
*/
|
||||
private function writeError(string $message): void
|
||||
{
|
||||
$this->writePestTestOutput($message, 'fg-red, bold', '⨯');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given warning message to the console.
|
||||
*/
|
||||
private function writeWarning(string $message): void
|
||||
{
|
||||
$this->writePestTestOutput($message, 'fg-yellow, bold', '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the give message to the console.
|
||||
*/
|
||||
private function writePestTestOutput(string $message, string $color, string $symbol): void
|
||||
{
|
||||
$this->writeWithColor($color, "$symbol ", false);
|
||||
|
||||
@ -19,7 +19,7 @@ trait RetrievesValues
|
||||
*
|
||||
* @return TRetrievableValue|null
|
||||
*/
|
||||
private function retrieve(string $key, $value, $default = null)
|
||||
private function retrieve(string $key, mixed $value, mixed $default = null): mixed
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value[$key] ?? $default;
|
||||
|
||||
@ -8,157 +8,105 @@ use Closure;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\ExecutionOrderDependency;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* To avoid inheritance conflicts, all the fields related
|
||||
* to Pest only will be prefixed by double underscore.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait Testable
|
||||
{
|
||||
/**
|
||||
* The test case description. Contains the first
|
||||
* argument of global functions like `it` and `test`.
|
||||
*
|
||||
* @var string
|
||||
* The Test Case "test" closure.
|
||||
*/
|
||||
private $__description;
|
||||
private Closure $__test;
|
||||
|
||||
/**
|
||||
* Holds the test closure function.
|
||||
*
|
||||
* @var Closure
|
||||
* The Test Case "setUp" closure.
|
||||
*/
|
||||
private $__test;
|
||||
private ?Closure $__beforeEach = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared beforeEach ("set up") closure if one has been
|
||||
* defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
* The Test Case "tearDown" closure.
|
||||
*/
|
||||
private $beforeEach = null;
|
||||
private ?Closure $__afterEach = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared afterEach ("tear down") closure if one has been
|
||||
* defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
* The Test Case "setUpBeforeClass" closure.
|
||||
*/
|
||||
private $afterEach = null;
|
||||
private static ?Closure $__beforeAll = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared beforeAll ("set up before") closure if one has been
|
||||
* defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
* The test "tearDownAfterClass" closure.
|
||||
*/
|
||||
private static $beforeAll = null;
|
||||
private static ?Closure $__afterAll = null;
|
||||
|
||||
/**
|
||||
* Holds a global/shared afterAll ("tear down after") closure if one has
|
||||
* been defined.
|
||||
*
|
||||
* @var Closure|null
|
||||
* Resets the test case static properties.
|
||||
*/
|
||||
private static $afterAll = null;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the test case.
|
||||
*/
|
||||
public function __construct(Closure $test, string $description, array $data)
|
||||
public static function flush(): void
|
||||
{
|
||||
$this->__test = $test;
|
||||
$this->__description = $description;
|
||||
self::$beforeAll = null;
|
||||
self::$afterAll = null;
|
||||
|
||||
parent::__construct('__test', $data);
|
||||
self::$__beforeAll = null;
|
||||
self::$__afterAll = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the groups to the current test case.
|
||||
* Creates a new Test Case instance.
|
||||
*/
|
||||
public function addGroups(array $groups): void
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$groups = array_unique(array_merge($this->getGroups(), $groups));
|
||||
parent::__construct($name);
|
||||
|
||||
$this->setGroups($groups);
|
||||
$this->__test = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($name)->getClosure($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dependencies to the test case and map them to instances of ExecutionOrderDependency.
|
||||
* Adds a new "setUpBeforeClass" to the Test Case.
|
||||
*/
|
||||
public function addDependencies(array $tests): void
|
||||
{
|
||||
$className = get_class($this);
|
||||
|
||||
$tests = array_map(function (string $test) use ($className): ExecutionOrderDependency {
|
||||
if (strpos($test, '::') === false) {
|
||||
$test = "{$className}::{$test}";
|
||||
}
|
||||
|
||||
return new ExecutionOrderDependency($test, null, '');
|
||||
}, $tests);
|
||||
|
||||
$this->setDependencies($tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" before all test hook that will execute **before**
|
||||
* the test defined `beforeAll` hook(s).
|
||||
*/
|
||||
public function addBeforeAll(?Closure $hook): void
|
||||
public function __addBeforeAll(?Closure $hook): void
|
||||
{
|
||||
if (!$hook) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$beforeAll = (self::$beforeAll instanceof Closure)
|
||||
? ChainableClosure::fromStatic(self::$beforeAll, $hook)
|
||||
self::$__beforeAll = (self::$__beforeAll instanceof Closure)
|
||||
? ChainableClosure::fromStatic(self::$__beforeAll, $hook)
|
||||
: $hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" after all test hook that will execute **before**
|
||||
* the test defined `afterAll` hook(s).
|
||||
* Adds a new "tearDownAfterClass" to the Test Case.
|
||||
*/
|
||||
public function addAfterAll(?Closure $hook): void
|
||||
public function __addAfterAll(?Closure $hook): void
|
||||
{
|
||||
if (!$hook) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$afterAll = (self::$afterAll instanceof Closure)
|
||||
? ChainableClosure::fromStatic(self::$afterAll, $hook)
|
||||
self::$__afterAll = (self::$__afterAll instanceof Closure)
|
||||
? ChainableClosure::fromStatic(self::$__afterAll, $hook)
|
||||
: $hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" before each test hook that will execute **before**
|
||||
* the test defined `beforeEach` hook.
|
||||
* Adds a new "setUp" to the Test Case.
|
||||
*/
|
||||
public function addBeforeEach(?Closure $hook): void
|
||||
public function __addBeforeEach(?Closure $hook): void
|
||||
{
|
||||
$this->addHook('beforeEach', $hook);
|
||||
$this->__addHook('__beforeEach', $hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/"global" after each test hook that will execute **before**
|
||||
* the test defined `afterEach` hook.
|
||||
* Adds a new "tearDown" to the Test Case.
|
||||
*/
|
||||
public function addAfterEach(?Closure $hook): void
|
||||
public function __addAfterEach(?Closure $hook): void
|
||||
{
|
||||
$this->addHook('afterEach', $hook);
|
||||
$this->__addHook('__afterEach', $hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared/global hook and compose them if more than one is passed.
|
||||
* Adds a new "hook" to the Test Case.
|
||||
*/
|
||||
private function addHook(string $property, ?Closure $hook): void
|
||||
private function __addHook(string $property, ?Closure $hook): void
|
||||
{
|
||||
if (!$hook) {
|
||||
return;
|
||||
@ -170,22 +118,15 @@ trait Testable
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test case name. Note that, in Pest
|
||||
* we ignore withDataset argument as the description
|
||||
* already contains the dataset description.
|
||||
* Gets the Test Case filename.
|
||||
*/
|
||||
public function getName(bool $withDataSet = true): string
|
||||
{
|
||||
return $this->__description;
|
||||
}
|
||||
|
||||
public static function __getFileName(): string
|
||||
public static function __getFilename(): string
|
||||
{
|
||||
return self::$__filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before the first test of this test class is run.
|
||||
* This method is called before the first test of this Test Case is run.
|
||||
*/
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
@ -193,22 +134,22 @@ trait Testable
|
||||
|
||||
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
|
||||
|
||||
if (self::$beforeAll instanceof Closure) {
|
||||
$beforeAll = ChainableClosure::fromStatic(self::$beforeAll, $beforeAll);
|
||||
if (self::$__beforeAll instanceof Closure) {
|
||||
$beforeAll = ChainableClosure::fromStatic(self::$__beforeAll, $beforeAll);
|
||||
}
|
||||
|
||||
call_user_func(Closure::bind($beforeAll, null, self::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after the last test of this test class is run.
|
||||
* This method is called after the last test of this Test Case is run.
|
||||
*/
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
|
||||
|
||||
if (self::$afterAll instanceof Closure) {
|
||||
$afterAll = ChainableClosure::fromStatic(self::$afterAll, $afterAll);
|
||||
if (self::$__afterAll instanceof Closure) {
|
||||
$afterAll = ChainableClosure::fromStatic(self::$__afterAll, $afterAll);
|
||||
}
|
||||
|
||||
call_user_func(Closure::bind($afterAll, null, self::class));
|
||||
@ -217,7 +158,7 @@ trait Testable
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed before the test.
|
||||
* Gets executed before the Test Case.
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
@ -227,22 +168,22 @@ trait Testable
|
||||
|
||||
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
|
||||
|
||||
if ($this->beforeEach instanceof Closure) {
|
||||
$beforeEach = ChainableClosure::from($this->beforeEach, $beforeEach);
|
||||
if ($this->__beforeEach instanceof Closure) {
|
||||
$beforeEach = ChainableClosure::from($this->__beforeEach, $beforeEach);
|
||||
}
|
||||
|
||||
$this->__callClosure($beforeEach, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets executed after the test.
|
||||
* Gets executed after the Test Case.
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
|
||||
|
||||
if ($this->afterEach instanceof Closure) {
|
||||
$afterEach = ChainableClosure::from($this->afterEach, $afterEach);
|
||||
if ($this->__afterEach instanceof Closure) {
|
||||
$afterEach = ChainableClosure::from($this->__afterEach, $afterEach);
|
||||
}
|
||||
|
||||
$this->__callClosure($afterEach, func_get_args());
|
||||
@ -253,27 +194,13 @@ trait Testable
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the test case as string.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s::%s',
|
||||
self::$__filename,
|
||||
$this->__description
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the test.
|
||||
*
|
||||
* @return mixed
|
||||
* Executes the Test Case current test.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __test()
|
||||
private function __runTest(Closure $closure, ...$args): mixed
|
||||
{
|
||||
return $this->__callClosure($this->__test, $this->resolveTestArguments(func_get_args()));
|
||||
return $this->__callClosure($closure, $this->__resolveTestArguments($args));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -281,25 +208,22 @@ trait Testable
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function resolveTestArguments(array $arguments): array
|
||||
private function __resolveTestArguments(array $arguments): array
|
||||
{
|
||||
return array_map(function ($data) {
|
||||
return $data instanceof Closure ? $this->__callClosure($data, []) : $data;
|
||||
}, $arguments);
|
||||
return array_map(fn ($data) => $data instanceof Closure ? $this->__callClosure($data, []) : $data, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function __callClosure(Closure $closure, array $arguments)
|
||||
private function __callClosure(Closure $closure, array $arguments): mixed
|
||||
{
|
||||
return ExceptionTrace::ensure(function () use ($closure, $arguments) {
|
||||
return call_user_func_array(Closure::bind($closure, $this, get_class($this)), $arguments);
|
||||
});
|
||||
return ExceptionTrace::ensure(fn () => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Test Case name that should be used by printers.
|
||||
*/
|
||||
public function getPrintableTestCaseName(): string
|
||||
{
|
||||
return ltrim(self::class, 'P\\');
|
||||
|
||||
@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Console;
|
||||
|
||||
use Pest\Actions\AddsDefaults;
|
||||
use Pest\Actions\AddsTests;
|
||||
use Pest\Actions\InteractsWithPlugins;
|
||||
use Pest\Actions\LoadStructure;
|
||||
use Pest\Actions\ValidatesConfiguration;
|
||||
use Pest\Plugins\Version;
|
||||
use Pest\Support\Container;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestSuite as BaseTestSuite;
|
||||
use PHPUnit\TextUI\Command as BaseCommand;
|
||||
use PHPUnit\TextUI\TestRunner;
|
||||
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Command extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* Holds the current testing suite.
|
||||
*
|
||||
* @var TestSuite
|
||||
*/
|
||||
private $testSuite;
|
||||
|
||||
/**
|
||||
* Holds the current console output.
|
||||
*
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the command class.
|
||||
*/
|
||||
public function __construct(TestSuite $testSuite, OutputInterface $output)
|
||||
{
|
||||
$this->testSuite = $testSuite;
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @phpstan-ignore-next-line
|
||||
*
|
||||
* @param array<int, string> $argv
|
||||
*/
|
||||
protected function handleArguments(array $argv): void
|
||||
{
|
||||
$argv = InteractsWithPlugins::handleArguments($argv);
|
||||
|
||||
parent::handleArguments($argv);
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
AddsTests::to($testSuite, $this->testSuite);
|
||||
|
||||
return $testRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @phpstan-ignore-next-line
|
||||
*
|
||||
* @param array<int, string> $argv
|
||||
*/
|
||||
public function run(array $argv, bool $exit = true): int
|
||||
{
|
||||
LoadStructure::in($this->testSuite->rootPath);
|
||||
|
||||
$result = parent::run($argv, false);
|
||||
$result = InteractsWithPlugins::addOutput($result);
|
||||
|
||||
exit($result);
|
||||
}
|
||||
|
||||
protected function showHelp(): void
|
||||
{
|
||||
/** @var Version $version */
|
||||
$version = Container::getInstance()->get(Version::class);
|
||||
$version->handleArguments(['--version']);
|
||||
parent::showHelp();
|
||||
|
||||
(new Help($this->output))();
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,11 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
final class Help
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
/**
|
||||
* The Command messages.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const HELP_MESSAGES = [
|
||||
'<comment>Pest Options:</comment>',
|
||||
' <info>--init</info> Initialise a standard Pest configuration',
|
||||
@ -20,14 +24,17 @@ final class Help
|
||||
' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
|
||||
];
|
||||
|
||||
/** @var OutputInterface */
|
||||
private $output;
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
/**
|
||||
* Creates a new Console Command instance.
|
||||
*/
|
||||
public function __construct(private OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the Console Command.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
foreach (self::HELP_MESSAGES as $message) {
|
||||
|
||||
@ -14,7 +14,11 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
*/
|
||||
final class Thanks
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
/**
|
||||
* The Command messages.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const FUNDING_MESSAGES = [
|
||||
'',
|
||||
' - Star or contribute to Pest:',
|
||||
@ -25,16 +29,16 @@ final class Thanks
|
||||
' <options=bold>https://github.com/sponsors/nunomaduro</>',
|
||||
];
|
||||
|
||||
/** @var OutputInterface */
|
||||
private $output;
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
/**
|
||||
* Creates a new Console Command instance.
|
||||
*/
|
||||
public function __construct(private OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user to support Pest.
|
||||
* Executes the Console Command.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
|
||||
@ -4,18 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Contracts;
|
||||
|
||||
if (interface_exists(\NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName::class)) {
|
||||
/**
|
||||
use NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName as BaseHasPrintableTestCaseName;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface HasPrintableTestCaseName extends \NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName
|
||||
{
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface HasPrintableTestCaseName
|
||||
{
|
||||
}
|
||||
interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ namespace Pest\Contracts\Plugins;
|
||||
interface AddsOutput
|
||||
{
|
||||
/**
|
||||
* Allows to add custom output after the test suite was executed.
|
||||
* Adds output after the Test Suite execution.
|
||||
*/
|
||||
public function addOutput(int $testReturnCode): int;
|
||||
public function addOutput(int $exitCode): int;
|
||||
}
|
||||
|
||||
@ -10,11 +10,11 @@ namespace Pest\Contracts\Plugins;
|
||||
interface HandlesArguments
|
||||
{
|
||||
/**
|
||||
* Allows to handle custom command line arguments.
|
||||
* Adds arguments before of the Test Suite execution.
|
||||
*
|
||||
* @param array<int, string> $arguments
|
||||
* @param array<int, string> $argv
|
||||
*
|
||||
* @return array<int, string> the updated list of arguments
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function handleArguments(array $arguments): array;
|
||||
public function handleArguments(array $argv): array;
|
||||
}
|
||||
|
||||
@ -34,25 +34,23 @@ final class CoreExpectation
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var mixed
|
||||
* @var TValue
|
||||
*/
|
||||
public $value;
|
||||
public mixed $value;
|
||||
|
||||
/**
|
||||
* The exporter instance, if any.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var Exporter|null
|
||||
*/
|
||||
private $exporter;
|
||||
private Exporter|null $exporter;
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param TValue $value
|
||||
*/
|
||||
public function __construct($value)
|
||||
public function __construct(mixed $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
@ -61,10 +59,8 @@ final class CoreExpectation
|
||||
* Asserts that two variables have the same type and
|
||||
* value. Used on objects, it asserts that two
|
||||
* variables reference the same object.
|
||||
*
|
||||
* @param mixed $expected
|
||||
*/
|
||||
public function toBe($expected): CoreExpectation
|
||||
public function toBe(mixed $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertSame($expected, $this->value);
|
||||
|
||||
@ -123,10 +119,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value is greater than $expected.
|
||||
*
|
||||
* @param int|float $expected
|
||||
*/
|
||||
public function toBeGreaterThan($expected): CoreExpectation
|
||||
public function toBeGreaterThan(int|float $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertGreaterThan($expected, $this->value);
|
||||
|
||||
@ -135,10 +129,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value is greater than or equal to $expected.
|
||||
*
|
||||
* @param int|float $expected
|
||||
*/
|
||||
public function toBeGreaterThanOrEqual($expected): CoreExpectation
|
||||
public function toBeGreaterThanOrEqual(int|float $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertGreaterThanOrEqual($expected, $this->value);
|
||||
|
||||
@ -147,10 +139,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value is less than or equal to $expected.
|
||||
*
|
||||
* @param int|float $expected
|
||||
*/
|
||||
public function toBeLessThan($expected): CoreExpectation
|
||||
public function toBeLessThan(int|float $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertLessThan($expected, $this->value);
|
||||
|
||||
@ -159,10 +149,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value is less than $expected.
|
||||
*
|
||||
* @param int|float $expected
|
||||
*/
|
||||
public function toBeLessThanOrEqual($expected): CoreExpectation
|
||||
public function toBeLessThanOrEqual(int|float $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertLessThanOrEqual($expected, $this->value);
|
||||
|
||||
@ -171,10 +159,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that $needle is an element of the value.
|
||||
*
|
||||
* @param mixed $needles
|
||||
*/
|
||||
public function toContain(...$needles): CoreExpectation
|
||||
public function toContain(mixed ...$needles): CoreExpectation
|
||||
{
|
||||
foreach ($needles as $needle) {
|
||||
if (is_string($this->value)) {
|
||||
@ -189,6 +175,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value starts with $expected.
|
||||
*
|
||||
* @param non-empty-string $expected
|
||||
*/
|
||||
public function toStartWith(string $expected): CoreExpectation
|
||||
{
|
||||
@ -199,6 +187,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value ends with $expected.
|
||||
*
|
||||
* @param non-empty-string $expected
|
||||
*/
|
||||
public function toEndWith(string $expected): CoreExpectation
|
||||
{
|
||||
@ -234,7 +224,7 @@ final class CoreExpectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('Expectation value length is not countable.');
|
||||
throw new BadMethodCallException('CoreExpectation value length is not countable.');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -249,10 +239,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value contains the property $name.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function toHaveProperty(string $name, $value = null): CoreExpectation
|
||||
public function toHaveProperty(string $name, mixed $value = null): CoreExpectation
|
||||
{
|
||||
$this->toBeObject();
|
||||
|
||||
@ -282,10 +270,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that two variables have the same value.
|
||||
*
|
||||
* @param mixed $expected
|
||||
*/
|
||||
public function toEqual($expected): CoreExpectation
|
||||
public function toEqual(mixed $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertEquals($expected, $this->value);
|
||||
|
||||
@ -300,10 +286,8 @@ final class CoreExpectation
|
||||
* are sorted before they are compared. When $expected and $this->value
|
||||
* are objects, each object is converted to an array containing all
|
||||
* private, protected and public attributes.
|
||||
*
|
||||
* @param mixed $expected
|
||||
*/
|
||||
public function toEqualCanonicalizing($expected): CoreExpectation
|
||||
public function toEqualCanonicalizing(mixed $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertEqualsCanonicalizing($expected, $this->value);
|
||||
|
||||
@ -313,10 +297,8 @@ final class CoreExpectation
|
||||
/**
|
||||
* Asserts that the absolute difference between the value and $expected
|
||||
* is lower than $delta.
|
||||
*
|
||||
* @param mixed $expected
|
||||
*/
|
||||
public function toEqualWithDelta($expected, float $delta): CoreExpectation
|
||||
public function toEqualWithDelta(mixed $expected, float $delta): CoreExpectation
|
||||
{
|
||||
Assert::assertEqualsWithDelta($expected, $this->value, $delta);
|
||||
|
||||
@ -347,10 +329,11 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value is an instance of $class.
|
||||
*
|
||||
* @param class-string $class
|
||||
*/
|
||||
public function toBeInstanceOf(string $class): CoreExpectation
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
Assert::assertInstanceOf($class, $this->value);
|
||||
|
||||
return $this;
|
||||
@ -499,11 +482,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value array has the provided $key.
|
||||
*
|
||||
* @param string|int $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function toHaveKey($key, $value = null): CoreExpectation
|
||||
public function toHaveKey(string|int $key, mixed $value = null): CoreExpectation
|
||||
{
|
||||
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
|
||||
$array = $this->value->toArray();
|
||||
@ -513,8 +493,6 @@ final class CoreExpectation
|
||||
|
||||
try {
|
||||
Assert::assertTrue(Arr::has($array, $key));
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
} catch (ExpectationFailedException $exception) {
|
||||
throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure());
|
||||
}
|
||||
@ -603,9 +581,9 @@ final class CoreExpectation
|
||||
/**
|
||||
* Asserts that the value array matches the given array subset.
|
||||
*
|
||||
* @param array<int|string, mixed> $array
|
||||
* @phpstan-param iterable<int|string, mixed> $array
|
||||
*/
|
||||
public function toMatchArray(array $array): CoreExpectation
|
||||
public function toMatchArray(iterable|object $array): CoreExpectation
|
||||
{
|
||||
if (is_object($this->value) && method_exists($this->value, 'toArray')) {
|
||||
$valueAsArray = $this->value->toArray();
|
||||
@ -634,9 +612,9 @@ final class CoreExpectation
|
||||
* Asserts that the value object matches a subset
|
||||
* of the properties of an given object.
|
||||
*
|
||||
* @param array<string, mixed>|object $object
|
||||
* @phpstan-param iterable<string, mixed>|object $object
|
||||
*/
|
||||
public function toMatchObject($object): CoreExpectation
|
||||
public function toMatchObject(iterable|object $object): CoreExpectation
|
||||
{
|
||||
foreach ((array) $object as $property => $value) {
|
||||
Assert::assertTrue(property_exists($this->value, $property));
|
||||
@ -672,6 +650,7 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toMatchConstraint(Constraint $constraint): CoreExpectation
|
||||
{
|
||||
|
||||
Assert::assertThat($this->value, $constraint);
|
||||
|
||||
return $this;
|
||||
@ -680,9 +659,9 @@ final class CoreExpectation
|
||||
/**
|
||||
* Asserts that executing value throws an exception.
|
||||
*
|
||||
* @param (Closure(Throwable): mixed)|string $exception
|
||||
* @phpstan-param (Closure(Throwable): mixed)|string $exception
|
||||
*/
|
||||
public function toThrow($exception, string $exceptionMessage = null): CoreExpectation
|
||||
public function toThrow(callable|string $exception, string $exceptionMessage = null): CoreExpectation
|
||||
{
|
||||
$callback = NullClosure::create();
|
||||
|
||||
@ -729,10 +708,8 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Exports the given value.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function export($value): string
|
||||
private function export(mixed $value): string
|
||||
{
|
||||
if ($this->exporter === null) {
|
||||
$this->exporter = new Exporter();
|
||||
|
||||
@ -20,14 +20,21 @@ final class Datasets
|
||||
*
|
||||
* @var array<int|string, Closure|iterable<int|string, mixed>>
|
||||
*/
|
||||
private static $datasets = [];
|
||||
private static array $datasets = [];
|
||||
|
||||
/**
|
||||
* Holds the withs.
|
||||
*
|
||||
* @var array<string, \Closure|iterable|string>
|
||||
*/
|
||||
private static array $withs = [];
|
||||
|
||||
/**
|
||||
* Sets the given.
|
||||
*
|
||||
* @param Closure|iterable<int|string, mixed> $data
|
||||
*/
|
||||
public static function set(string $name, $data): void
|
||||
public static function set(string $name, Closure|iterable $data): void
|
||||
{
|
||||
if (array_key_exists($name, self::$datasets)) {
|
||||
throw new DatasetAlreadyExist($name);
|
||||
@ -37,34 +44,42 @@ final class Datasets
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure|iterable<int|string, mixed>
|
||||
* Sets the given.
|
||||
*
|
||||
* @param Closure|iterable<int|string, mixed>|string $with
|
||||
*/
|
||||
public static function get(string $name)
|
||||
public static function with(string $filename, string $description, Closure|iterable|string $with): void
|
||||
{
|
||||
if (!array_key_exists($name, self::$datasets)) {
|
||||
throw new DatasetDoesNotExist($name);
|
||||
self::$withs[$filename . '>>>' . $description] = $with;
|
||||
}
|
||||
|
||||
return self::$datasets[$name];
|
||||
/**
|
||||
* @return Closure|iterable<int|string, mixed>
|
||||
*/
|
||||
public static function get(string $filename, string $description): Closure|iterable
|
||||
{
|
||||
$dataset = self::$withs[$filename . '>>>' . $description];
|
||||
|
||||
return self::resolve($description, $dataset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the current dataset to an array value.
|
||||
*
|
||||
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
|
||||
* @param array<Closure|iterable<int|string, mixed>|string> $dataset
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public static function resolve(string $description, array $datasets): array
|
||||
public static function resolve(string $description, array $dataset): array|null
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (empty($datasets)) {
|
||||
return [$description => []];
|
||||
if (empty($dataset)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$datasets = self::processDatasets($datasets);
|
||||
$dataset = self::processDatasets($dataset);
|
||||
|
||||
$datasetCombinations = self::getDataSetsCombinations($datasets);
|
||||
$datasetCombinations = self::getDataSetsCombinations($dataset);
|
||||
|
||||
$dataSetDescriptions = [];
|
||||
$dataSetValues = [];
|
||||
@ -114,7 +129,11 @@ final class Datasets
|
||||
$processedDataset = [];
|
||||
|
||||
if (is_string($data)) {
|
||||
$datasets[$index] = self::get($data);
|
||||
if (!array_key_exists($data, self::$datasets)) {
|
||||
throw new DatasetDoesNotExist($data);
|
||||
}
|
||||
|
||||
$datasets[$index] = self::$datasets[$data];
|
||||
}
|
||||
|
||||
if (is_callable($datasets[$index])) {
|
||||
@ -161,10 +180,9 @@ final class Datasets
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string $key
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
private static function getDataSetDescription($key, array $data): string
|
||||
private static function getDataSetDescription(int|string $key, array $data): string
|
||||
{
|
||||
$exporter = new Exporter();
|
||||
|
||||
|
||||
18
src/Each.php
18
src/Each.php
@ -11,30 +11,20 @@ namespace Pest;
|
||||
*/
|
||||
final class Each
|
||||
{
|
||||
/**
|
||||
* @var Expectation
|
||||
*/
|
||||
private $original;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $opposite = false;
|
||||
private bool $opposite = false;
|
||||
|
||||
/**
|
||||
* Creates an expectation on each item of the iterable "value".
|
||||
*/
|
||||
public function __construct(Expectation $original)
|
||||
public function __construct(private Expectation $original)
|
||||
{
|
||||
$this->original = $original;
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function and($value): Expectation
|
||||
public function and(mixed $value): Expectation
|
||||
{
|
||||
return $this->original->and($value);
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of after all already exist exception.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class AfterEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of after each already exist exception.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class AttributeNotSupportedYet extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of attribute not supported yet.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $attribute, string $value)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class BeforeEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of before each already exist exception.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class DatasetAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of dataset already exist.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of dataset does not exist.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $name)
|
||||
{
|
||||
|
||||
@ -10,26 +10,22 @@ use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
|
||||
/**
|
||||
* Creates a new instance of dataset is not present for test that has arguments.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Create new exception instance.
|
||||
* Creates a new Exception instance.
|
||||
*
|
||||
* @param array<string, string> $args A map of argument names to their typee
|
||||
* @param array<string, string> $arguments
|
||||
*/
|
||||
public function __construct(string $file, string $name, array $args)
|
||||
public function __construct(string $file, string $name, array $arguments)
|
||||
{
|
||||
parent::__construct(sprintf(
|
||||
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
|
||||
$name,
|
||||
count($args),
|
||||
implode(', ', array_map(static function (string $arg, string $type): string {
|
||||
return sprintf('%s $%s', $type, $arg);
|
||||
}, array_keys($args), $args)),
|
||||
count($arguments),
|
||||
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
|
||||
$file,
|
||||
));
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of file not found.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $filename)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class InvalidConsoleArgument extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of should not happen.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of invalid pest command exception.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of missing dependency.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $feature, string $dependency)
|
||||
{
|
||||
|
||||
@ -13,7 +13,7 @@ use RuntimeException;
|
||||
final class ShouldNotHappen extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of should not happen.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(Exception $exception)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of test already exist.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $fileName, string $description)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of test case already in use.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $inUse, string $newOne, string $folder)
|
||||
{
|
||||
|
||||
@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||
{
|
||||
/**
|
||||
* Creates a new instance of after each already exist exception.
|
||||
* Creates a new Exception instance.
|
||||
*/
|
||||
public function __construct(string $testCaseClass)
|
||||
{
|
||||
|
||||
@ -25,20 +25,20 @@ use PHPUnit\Framework\ExpectationFailedException;
|
||||
*/
|
||||
final class Expectation
|
||||
{
|
||||
use Extendable {
|
||||
use RetrievesValues, Extendable {
|
||||
__call as __extendsCall;
|
||||
}
|
||||
|
||||
use RetrievesValues;
|
||||
|
||||
/** @var CoreExpectation */
|
||||
private $coreExpectation;
|
||||
private CoreExpectation $coreExpectation;
|
||||
|
||||
/**
|
||||
* Creates a new Expectation.
|
||||
*
|
||||
* @param TValue $value
|
||||
*/
|
||||
public function __construct($value)
|
||||
public function __construct(mixed $value)
|
||||
{
|
||||
$this->coreExpectation = new CoreExpectation($value);
|
||||
}
|
||||
@ -50,7 +50,7 @@ final class Expectation
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function and($value): Expectation
|
||||
public function and(mixed $value): Expectation
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
@ -66,11 +66,9 @@ final class Expectation
|
||||
/**
|
||||
* Dump the expectation value and end the script.
|
||||
*
|
||||
* @param mixed $arguments
|
||||
*
|
||||
* @return never
|
||||
* @phpstan-return never
|
||||
*/
|
||||
public function dd(...$arguments): void
|
||||
public function dd(mixed ...$arguments): void
|
||||
{
|
||||
if (function_exists('dd')) {
|
||||
dd($this->value, ...$arguments);
|
||||
@ -83,13 +81,10 @@ final class Expectation
|
||||
|
||||
/**
|
||||
* Send the expectation value to Ray along with all given arguments.
|
||||
*
|
||||
* @param mixed $arguments
|
||||
*/
|
||||
public function ray(...$arguments): self
|
||||
public function ray(mixed ...$arguments): self
|
||||
{
|
||||
if (function_exists('ray')) {
|
||||
// @phpstan-ignore-next-line
|
||||
ray($this->value, ...$arguments);
|
||||
}
|
||||
|
||||
@ -127,11 +122,9 @@ final class Expectation
|
||||
*
|
||||
* @template TSequenceValue
|
||||
*
|
||||
* @param callable(self, self): void|TSequenceValue ...$callbacks
|
||||
*
|
||||
* @noinspection PhpParamsInspection
|
||||
* @phpstan-param (callable(self, self): void)|TSequenceValue ...$callbacks
|
||||
*/
|
||||
public function sequence(...$callbacks): Expectation
|
||||
public function sequence(mixed ...$callbacks): Expectation
|
||||
{
|
||||
if (!is_iterable($this->value)) {
|
||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||
@ -170,16 +163,14 @@ final class Expectation
|
||||
*
|
||||
* @template TMatchSubject of array-key
|
||||
*
|
||||
* @param callable(): TMatchSubject|TMatchSubject $subject
|
||||
* @param (callable(): TMatchSubject)|TMatchSubject $subject
|
||||
* @param array<TMatchSubject, (callable(Expectation<TValue>): mixed)|TValue> $expressions
|
||||
*/
|
||||
public function match($subject, array $expressions): Expectation
|
||||
public function match(mixed $subject, array $expressions): Expectation
|
||||
{
|
||||
$subject = is_callable($subject)
|
||||
? $subject
|
||||
: function () use ($subject) {
|
||||
return $subject;
|
||||
};
|
||||
: fn () => $subject;
|
||||
|
||||
$subject = $subject();
|
||||
|
||||
@ -212,15 +203,15 @@ final class Expectation
|
||||
/**
|
||||
* Apply the callback if the given "condition" is falsy.
|
||||
*
|
||||
* @param (callable(): bool)|bool $condition
|
||||
* @param callable(Expectation<TValue>): mixed $callback
|
||||
* @phpstan-param (callable(): bool)|bool $condition
|
||||
* @phpstan-param callable(Expectation<TValue>): mixed $callback
|
||||
*/
|
||||
public function unless($condition, callable $callback): Expectation
|
||||
public function unless(callable|bool $condition, callable $callback): Expectation
|
||||
{
|
||||
$condition = is_callable($condition)
|
||||
? $condition
|
||||
: static function () use ($condition): bool {
|
||||
return (bool) $condition; // @phpstan-ignore-line
|
||||
return $condition;
|
||||
};
|
||||
|
||||
return $this->when(!$condition(), $callback);
|
||||
@ -229,15 +220,15 @@ final class Expectation
|
||||
/**
|
||||
* Apply the callback if the given "condition" is truthy.
|
||||
*
|
||||
* @param (callable(): bool)|bool $condition
|
||||
* @param callable(Expectation<TValue>): mixed $callback
|
||||
* @phpstan-param (callable(): bool)|bool $condition
|
||||
* @phpstan-param callable(Expectation<TValue>): mixed $callback
|
||||
*/
|
||||
public function when($condition, callable $callback): Expectation
|
||||
public function when(callable|bool $condition, callable $callback): Expectation
|
||||
{
|
||||
$condition = is_callable($condition)
|
||||
? $condition
|
||||
: static function () use ($condition): bool {
|
||||
return (bool) $condition; // @phpstan-ignore-line
|
||||
return $condition;
|
||||
};
|
||||
|
||||
if ($condition()) {
|
||||
@ -251,7 +242,7 @@ final class Expectation
|
||||
* Dynamically handle calls to the class or
|
||||
* creates a new higher order expectation.
|
||||
*
|
||||
* @param array<int, mixed> $parameters
|
||||
* @phpstan-param array<int, mixed> $parameters
|
||||
*
|
||||
* @return HigherOrderExpectation|Expectation
|
||||
*/
|
||||
@ -288,6 +279,7 @@ final class Expectation
|
||||
throw PipeException::expectationNotFound($name);
|
||||
}
|
||||
|
||||
|
||||
private function hasExpectation(string $name): bool
|
||||
{
|
||||
if (method_exists($this->coreExpectation, $name)) {
|
||||
@ -304,10 +296,8 @@ final class Expectation
|
||||
/**
|
||||
* Dynamically calls methods on the class without any arguments
|
||||
* or creates a new higher order expectation.
|
||||
*
|
||||
* @return Expectation|HigherOrderExpectation
|
||||
*/
|
||||
public function __get(string $name)
|
||||
public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation
|
||||
{
|
||||
if ($name === 'value') {
|
||||
return $this->coreExpectation->value;
|
||||
|
||||
28
src/Factories/Annotations/Depends.php
Normal file
28
src/Factories/Annotations/Depends.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories\Annotations;
|
||||
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Support\Str;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Depends
|
||||
{
|
||||
/**
|
||||
* Adds annotations regarding the "depends" feature.
|
||||
*/
|
||||
public function add(TestCaseMethodFactory $method, array $annotations): array
|
||||
{
|
||||
foreach ($method->depends as $depend) {
|
||||
$depend = Str::evaluable($depend);
|
||||
|
||||
$annotations[] = "@depends $depend";
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
}
|
||||
25
src/Factories/Annotations/Groups.php
Normal file
25
src/Factories/Annotations/Groups.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories\Annotations;
|
||||
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Groups
|
||||
{
|
||||
/**
|
||||
* Adds annotations regarding the "groups" feature.
|
||||
*/
|
||||
public function add(TestCaseMethodFactory $method, array $annotations): array
|
||||
{
|
||||
foreach ($method->groups as $group) {
|
||||
$annotations[] = "@group $group";
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
}
|
||||
35
src/Factories/Concerns/HigherOrderable.php
Normal file
35
src/Factories/Concerns/HigherOrderable.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories\Concerns;
|
||||
|
||||
use Pest\Support\HigherOrderMessageCollection;
|
||||
|
||||
trait HigherOrderable
|
||||
{
|
||||
/**
|
||||
* The higher order messages that are chainable.
|
||||
*/
|
||||
public HigherOrderMessageCollection $chains;
|
||||
|
||||
/**
|
||||
* The higher order messages that are "factory" proxyable.
|
||||
*/
|
||||
public HigherOrderMessageCollection $factoryProxies;
|
||||
|
||||
/**
|
||||
* The higher order messages that are proxyable.
|
||||
*/
|
||||
public HigherOrderMessageCollection $proxies;
|
||||
|
||||
/**
|
||||
* Boot the higher order properties.
|
||||
*/
|
||||
private function bootHigherOrderable(): void
|
||||
{
|
||||
$this->chains = new HigherOrderMessageCollection();
|
||||
$this->factoryProxies = new HigherOrderMessageCollection();
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
}
|
||||
}
|
||||
@ -4,16 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories;
|
||||
|
||||
use Closure;
|
||||
use ParseError;
|
||||
use Pest\Concerns;
|
||||
use Pest\Contracts\HasPrintableTestCaseName;
|
||||
use Pest\Datasets;
|
||||
use Pest\Exceptions\DatasetMissing;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Support\HigherOrderMessageCollection;
|
||||
use Pest\Exceptions\TestAlreadyExist;
|
||||
use Pest\Factories\Concerns\HigherOrderable;
|
||||
use Pest\Plugins\Environment;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\Str;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
@ -22,159 +24,86 @@ use RuntimeException;
|
||||
*/
|
||||
final class TestCaseFactory
|
||||
{
|
||||
/**
|
||||
* Holds the test filename.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $filename;
|
||||
use HigherOrderable;
|
||||
|
||||
/**
|
||||
* Marks this test case as only.
|
||||
* The list of annotations.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var bool
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
public $only = false;
|
||||
private static array $annotations = [
|
||||
Annotations\Depends::class,
|
||||
Annotations\Groups::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Holds the test description.
|
||||
* The FQN of the Test Case class.
|
||||
*
|
||||
* If the description is null, means that it
|
||||
* will be created with the given assertions.
|
||||
*
|
||||
* @var string|null
|
||||
* @var class-string
|
||||
*/
|
||||
public $description;
|
||||
public string $class = TestCase::class;
|
||||
|
||||
/**
|
||||
* Holds the test closure.
|
||||
* The list of class methods.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var Closure
|
||||
* @var array<string, TestCaseMethodFactory>
|
||||
*/
|
||||
public $test;
|
||||
public array $methods = [];
|
||||
|
||||
/**
|
||||
* Holds the dataset, if any.
|
||||
* The list of class traits.
|
||||
*
|
||||
* @var array<Closure|iterable<int|string, mixed>|string>
|
||||
* @var array <int, class-string>
|
||||
*/
|
||||
public $datasets = [];
|
||||
|
||||
/**
|
||||
* The FQN of the test case class.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $class = TestCase::class;
|
||||
|
||||
/**
|
||||
* An array of FQN of the class traits.
|
||||
*
|
||||
* @var array <int, string>
|
||||
*/
|
||||
public $traits = [
|
||||
public array $traits = [
|
||||
Concerns\Testable::class,
|
||||
Concerns\Expectable::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Holds the higher order messages
|
||||
* for the factory that are proxyble.
|
||||
*
|
||||
* @var HigherOrderMessageCollection
|
||||
* Creates a new Factory instance.
|
||||
*/
|
||||
public $factoryProxies;
|
||||
public function __construct(
|
||||
public string $filename
|
||||
) {
|
||||
$this->bootHigherOrderable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = null, Closure $closure = null)
|
||||
public function make(): void
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->description = $description;
|
||||
$this->test = $closure ?? function (): void {
|
||||
if (Assert::getCount() === 0) {
|
||||
self::markTestIncomplete(); // @phpstan-ignore-line
|
||||
}
|
||||
};
|
||||
$methodsUsingOnly = $this->methodsUsingOnly();
|
||||
|
||||
$this->factoryProxies = new HigherOrderMessageCollection();
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
$this->chains = new HigherOrderMessageCollection();
|
||||
$methods = array_filter($this->methods, function ($method) use ($methodsUsingOnly) {
|
||||
return count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true);
|
||||
});
|
||||
|
||||
if (count($methods) > 0) {
|
||||
$this->evaluate($this->filename, $methods);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the anonymous test case.
|
||||
* Returns all the "only" methods.
|
||||
*
|
||||
* @return array<int, TestCase>
|
||||
* @return array<int, TestCaseMethodFactory>
|
||||
*/
|
||||
public function build(TestSuite $testSuite): array
|
||||
public function methodsUsingOnly(): array
|
||||
{
|
||||
if ($this->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('Description can not be empty.');
|
||||
if (Environment::name() === Environment::CI) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$chains = $this->chains;
|
||||
$proxies = $this->proxies;
|
||||
$factoryTest = $this->test;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
$test = function () use ($chains, $proxies, $factoryTest) {
|
||||
$proxies->proxy($this);
|
||||
$chains->chain($this);
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
return 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->datasets);
|
||||
|
||||
return array_map($createTest, array_keys($datasets), $datasets);
|
||||
return array_values(array_filter($this->methods, static fn ($method): bool => $method->only));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a fully qualified class name from the given filename.
|
||||
* Creates a Test Case class using a runtime evaluate.
|
||||
*/
|
||||
public function makeClassFromFilename(string $filename): string
|
||||
public function evaluate(string $filename, array $methods): string
|
||||
{
|
||||
if ('\\' === DIRECTORY_SEPARATOR) {
|
||||
// In case Windows, strtolower drive name, like in UsesCall.
|
||||
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', function ($match): string {
|
||||
return strtolower($match['drive']);
|
||||
}, $filename);
|
||||
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $filename);
|
||||
}
|
||||
|
||||
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
|
||||
@ -186,9 +115,7 @@ final class TestCaseFactory
|
||||
// Strip out any %-encoded octets.
|
||||
$relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath);
|
||||
// Remove escaped quote sequences (maintain namespace)
|
||||
$relativePath = str_replace(array_map(function (string $quote): string {
|
||||
return sprintf('\\%s', $quote);
|
||||
}, ['\'', '"']), '', $relativePath);
|
||||
$relativePath = str_replace(array_map(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath);
|
||||
// Limit to A-Z, a-z, 0-9, '_', '-'.
|
||||
$relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath);
|
||||
|
||||
@ -198,9 +125,9 @@ final class TestCaseFactory
|
||||
}
|
||||
|
||||
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
|
||||
$traitsCode = sprintf('use %s;', implode(', ', array_map(function ($trait): string {
|
||||
return sprintf('\%s', $trait);
|
||||
}, $this->traits)));
|
||||
$traitsCode = sprintf('use %s;', implode(', ', array_map(
|
||||
static fn ($trait): string => sprintf('\%s', $trait), $this->traits))
|
||||
);
|
||||
|
||||
$partsFQN = explode('\\', $classFQN);
|
||||
$className = array_pop($partsFQN);
|
||||
@ -212,14 +139,65 @@ final class TestCaseFactory
|
||||
$classFQN .= $className;
|
||||
}
|
||||
|
||||
$methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string {
|
||||
$methodName = Str::evaluable($method->description);
|
||||
|
||||
$datasetsCode = '';
|
||||
$annotations = ['@test'];
|
||||
|
||||
foreach (self::$annotations as $annotation) {
|
||||
$annotations = (new $annotation())->add($method, $annotations);
|
||||
}
|
||||
|
||||
if (count($method->datasets) > 0) {
|
||||
$dataProviderName = $methodName . '_dataset';
|
||||
$annotations[] = "@dataProvider $dataProviderName";
|
||||
|
||||
Datasets::with($method->filename, $methodName, $method->datasets);
|
||||
|
||||
$datasetsCode = <<<EOF
|
||||
|
||||
public function $dataProviderName()
|
||||
{
|
||||
return __PestDatasets::get(self::\$__filename, "$methodName");
|
||||
}
|
||||
|
||||
EOF;
|
||||
}
|
||||
|
||||
$annotations = implode('', array_map(
|
||||
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
|
||||
));
|
||||
|
||||
return <<<EOF
|
||||
|
||||
/**$annotations
|
||||
*/
|
||||
public function $methodName()
|
||||
{
|
||||
return \$this->__runTest(
|
||||
\$this->__test,
|
||||
...func_get_args(),
|
||||
);
|
||||
}
|
||||
|
||||
$datasetsCode
|
||||
EOF;
|
||||
}, $methods));
|
||||
|
||||
try {
|
||||
eval("
|
||||
namespace $namespace;
|
||||
|
||||
use Pest\Datasets as __PestDatasets;
|
||||
use Pest\TestSuite as __PestTestSuite;
|
||||
|
||||
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
||||
$traitsCode
|
||||
|
||||
private static \$__filename = '$filename';
|
||||
|
||||
$methodsCode
|
||||
}
|
||||
");
|
||||
} catch (ParseError $caught) {
|
||||
@ -230,11 +208,40 @@ final class TestCaseFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test case will receive argument input from Pest, or not.
|
||||
* Adds the given Method to the Test Case.
|
||||
*/
|
||||
public function receivesArguments(): bool
|
||||
public function addMethod(TestCaseMethodFactory $method): void
|
||||
{
|
||||
return count($this->datasets) > 0
|
||||
|| $this->factoryProxies->count('addDependencies') > 0;
|
||||
if ($method->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||
}
|
||||
|
||||
if (array_key_exists($method->description, $this->methods)) {
|
||||
throw new TestAlreadyExist($method->filename, $method->description);
|
||||
}
|
||||
|
||||
if (!$method->receivesArguments()) {
|
||||
$arguments = Reflection::getFunctionArguments($method->closure);
|
||||
|
||||
if (count($arguments) > 0) {
|
||||
throw new DatasetMissing($method->filename, $method->description, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
$this->methods[$method->description] = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Method by the given name.
|
||||
*/
|
||||
public function getMethod(string $methodName): TestCaseMethodFactory
|
||||
{
|
||||
foreach ($this->methods as $method) {
|
||||
if (Str::evaluable($method->description) === $methodName) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName));
|
||||
}
|
||||
}
|
||||
|
||||
103
src/Factories/TestCaseMethodFactory.php
Normal file
103
src/Factories/TestCaseMethodFactory.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Factories\Concerns\HigherOrderable;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class TestCaseMethodFactory
|
||||
{
|
||||
use HigherOrderable;
|
||||
/**
|
||||
* Determines if the Test Case will be the "only" being run.
|
||||
*/
|
||||
public bool $only = false;
|
||||
|
||||
/**
|
||||
* The Test Case Dataset, if any.
|
||||
*
|
||||
* @var array<Closure|iterable<int|string, mixed>|string>
|
||||
*/
|
||||
public array $datasets = [];
|
||||
|
||||
/**
|
||||
* The Test Case depends, if any.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $depends = [];
|
||||
|
||||
/**
|
||||
* The Test Case groups, if any.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $groups = [];
|
||||
|
||||
/**
|
||||
* Creates a new Factory instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
public ?string $description,
|
||||
public ?Closure $closure,
|
||||
) {
|
||||
if ($this->closure === null) {
|
||||
$this->closure = function () {
|
||||
Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line
|
||||
};
|
||||
}
|
||||
|
||||
$this->bootHigherOrderable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the Test Case classes.
|
||||
*/
|
||||
public function getClosure(TestCase $concrete): Closure
|
||||
{
|
||||
$concrete::flush(); // @phpstan-ignore-line
|
||||
|
||||
if ($this->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('Description can not be empty.');
|
||||
}
|
||||
|
||||
$closure = $this->closure;
|
||||
|
||||
$testCase = TestSuite::getInstance()->tests->get($this->filename);
|
||||
|
||||
$testCase->factoryProxies->proxy($concrete);
|
||||
$this->factoryProxies->proxy($concrete);
|
||||
|
||||
$method = $this;
|
||||
|
||||
return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
|
||||
/* @var TestCase $this */
|
||||
|
||||
$testCase->proxies->proxy($this);
|
||||
$method->proxies->proxy($this);
|
||||
|
||||
$testCase->chains->chain($this);
|
||||
$method->chains->chain($this);
|
||||
|
||||
return call_user_func(Closure::bind($closure, $this, $this::class), ...func_get_args());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test case will receive argument input from Pest, or not.
|
||||
*/
|
||||
public function receivesArguments(): bool
|
||||
{
|
||||
return count($this->datasets) > 0 || count($this->depends) > 0;
|
||||
}
|
||||
}
|
||||
@ -4,30 +4,30 @@ declare(strict_types=1);
|
||||
|
||||
use Pest\Datasets;
|
||||
use Pest\Expectation;
|
||||
use Pest\PendingObjects\AfterEachCall;
|
||||
use Pest\PendingObjects\BeforeEachCall;
|
||||
use Pest\PendingObjects\TestCall;
|
||||
use Pest\PendingObjects\UsesCall;
|
||||
use Pest\PendingCalls\AfterEachCall;
|
||||
use Pest\PendingCalls\BeforeEachCall;
|
||||
use Pest\PendingCalls\TestCall;
|
||||
use Pest\PendingCalls\UsesCall;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\Extendable;
|
||||
use Pest\Support\HigherOrderTapProxy;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
if (!function_exists('expect')) {
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
* @param mixed $value the Value
|
||||
*
|
||||
* @return Expectation|Extendable
|
||||
*/
|
||||
function expect($value = null)
|
||||
{
|
||||
function expect($value = null): Expectation|Extendable
|
||||
{
|
||||
if (func_num_args() === 0) {
|
||||
return new Extendable(Expectation::class);
|
||||
}
|
||||
|
||||
return new Expectation($value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('beforeAll')) {
|
||||
@ -60,7 +60,7 @@ if (!function_exists('dataset')) {
|
||||
*
|
||||
* @param Closure|iterable<int|string, mixed> $dataset
|
||||
*/
|
||||
function dataset(string $name, $dataset): void
|
||||
function dataset(string $name, Closure|iterable $dataset): void
|
||||
{
|
||||
Datasets::set($name, $dataset);
|
||||
}
|
||||
|
||||
@ -17,39 +17,17 @@ final class HigherOrderExpectation
|
||||
use Expectable;
|
||||
use RetrievesValues;
|
||||
|
||||
/**
|
||||
* @var Expectation
|
||||
*/
|
||||
private $original;
|
||||
private Expectation|Each $expectation;
|
||||
|
||||
/**
|
||||
* @var Expectation|Each
|
||||
*/
|
||||
private $expectation;
|
||||
private bool $opposite = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $opposite = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $shouldReset = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
private bool $shouldReset = false;
|
||||
|
||||
/**
|
||||
* Creates a new higher order expectation.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(Expectation $original, $value)
|
||||
public function __construct(private Expectation $original, mixed $value)
|
||||
{
|
||||
$this->original = $original;
|
||||
$this->expectation = $this->expect($value);
|
||||
}
|
||||
|
||||
@ -72,7 +50,7 @@ final class HigherOrderExpectation
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function and($value): Expectation
|
||||
public function and(mixed $value): Expectation
|
||||
{
|
||||
return $this->expect($value);
|
||||
}
|
||||
@ -118,10 +96,8 @@ final class HigherOrderExpectation
|
||||
|
||||
/**
|
||||
* Retrieve the applicable value based on the current reset condition.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function getValue()
|
||||
private function getValue(): mixed
|
||||
{
|
||||
return $this->shouldReset ? $this->original->value : $this->expectation->value;
|
||||
}
|
||||
|
||||
15
src/IgnorableTestCase.php
Normal file
15
src/IgnorableTestCase.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class IgnorableTestCase extends TestCase
|
||||
{
|
||||
// ..
|
||||
}
|
||||
69
src/Kernel.php
Normal file
69
src/Kernel.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use PHPUnit\TextUI\Application;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Kernel
|
||||
{
|
||||
/**
|
||||
* The Kernel bootstrappers.
|
||||
*
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
private static array $bootstrappers = [
|
||||
Bootstrappers\BootExceptionHandler::class,
|
||||
Bootstrappers\BootSubscribers::class,
|
||||
Bootstrappers\BootFiles::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates a new Kernel instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private Application $application
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Boots the Kernel.
|
||||
*/
|
||||
public static function boot(): self
|
||||
{
|
||||
foreach (self::$bootstrappers as $bootstrapper) {
|
||||
(new $bootstrapper())->__invoke();
|
||||
}
|
||||
|
||||
return new self(new Application());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given argv.
|
||||
*
|
||||
* @param array<int, string> $argv
|
||||
*/
|
||||
public function handle(array $argv): int
|
||||
{
|
||||
$argv = (new Plugins\Actions\HandleArguments())->__invoke($argv);
|
||||
|
||||
$result = $this->application->run(
|
||||
$argv, false,
|
||||
);
|
||||
|
||||
return (new Plugins\Actions\AddsOutput())->__invoke($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the Kernel.
|
||||
*/
|
||||
public function shutdown(): void
|
||||
{
|
||||
// ..
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,7 @@ use Pest\TestSuite;
|
||||
final class PestDatasetCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The console command name.
|
||||
* The Console Command name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
@ -25,7 +25,7 @@ final class PestDatasetCommand extends Command
|
||||
{--test-directory=tests : The name of the tests directory}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
* The Console Command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
|
||||
@ -14,7 +14,7 @@ use Pest\Laravel\Commands\PestTestCommand;
|
||||
final class PestServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register artisan commands.
|
||||
* Register Artisan Commands.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
|
||||
@ -12,419 +12,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Logging;
|
||||
|
||||
use function class_exists;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Exception;
|
||||
use function get_class;
|
||||
use function method_exists;
|
||||
use Pest\Concerns\Testable;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\ExceptionWrapper;
|
||||
use PHPUnit\Framework\SelfDescribing;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestFailure;
|
||||
use PHPUnit\Framework\TestListener;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\Util\Filter;
|
||||
use PHPUnit\Util\Printer;
|
||||
use PHPUnit\Util\Xml;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use Throwable;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class JUnit extends Printer implements TestListener
|
||||
final class JUnit extends Printer
|
||||
{
|
||||
/**
|
||||
* @var DOMDocument
|
||||
*/
|
||||
private $document;
|
||||
|
||||
/**
|
||||
* @var DOMElement
|
||||
*/
|
||||
private $root;
|
||||
|
||||
/**
|
||||
* @var DOMElement[]
|
||||
*/
|
||||
private $testSuites = [];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteTests = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteAssertions = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteErrors = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteWarnings = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteFailures = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private $testSuiteSkipped = [0];
|
||||
|
||||
/**
|
||||
* @var int[]|float[]
|
||||
*/
|
||||
private $testSuiteTimes = [0];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $testSuiteLevel = 0;
|
||||
|
||||
/**
|
||||
* @var DOMElement|null
|
||||
*/
|
||||
private $currentTestCase;
|
||||
|
||||
public function __construct(string $out)
|
||||
{
|
||||
$this->document = new DOMDocument('1.0', 'UTF-8');
|
||||
$this->document->formatOutput = true;
|
||||
|
||||
$this->root = $this->document->createElement('testsuites');
|
||||
$this->document->appendChild($this->root);
|
||||
|
||||
parent::__construct($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush buffer and close output.
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
$this->write($this->getXML());
|
||||
|
||||
parent::flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* An error occurred.
|
||||
*/
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $t, 'error');
|
||||
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning occurred.
|
||||
*/
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $e, 'warning');
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* A failure occurred.
|
||||
*/
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $e, 'failure');
|
||||
$this->testSuiteFailures[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incomplete test.
|
||||
*/
|
||||
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* Risky test.
|
||||
*/
|
||||
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Skipped test.
|
||||
*/
|
||||
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddSkipped();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$testSuite = $this->document->createElement('testsuite');
|
||||
$testSuite->setAttribute('name', $suite->getName());
|
||||
|
||||
if (class_exists($suite->getName(), false)) {
|
||||
try {
|
||||
$class = new ReflectionClass($suite->getName());
|
||||
|
||||
if ($class->hasMethod('__getFileName')) {
|
||||
$fileName = $class->getMethod('__getFileName')->invoke(null);
|
||||
} else {
|
||||
$fileName = $class->getFileName();
|
||||
}
|
||||
|
||||
$testSuite->setAttribute('file', $fileName);
|
||||
} catch (ReflectionException $e) {
|
||||
// @ignoreException
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->testSuiteLevel > 0) {
|
||||
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
|
||||
} else {
|
||||
$this->root->appendChild($testSuite);
|
||||
}
|
||||
|
||||
$this->testSuiteLevel++;
|
||||
$this->testSuites[$this->testSuiteLevel] = $testSuite;
|
||||
$this->testSuiteTests[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'tests',
|
||||
(string) $this->testSuiteTests[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'assertions',
|
||||
(string) $this->testSuiteAssertions[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'errors',
|
||||
(string) $this->testSuiteErrors[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'warnings',
|
||||
(string) $this->testSuiteWarnings[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'failures',
|
||||
(string) $this->testSuiteFailures[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'skipped',
|
||||
(string) $this->testSuiteSkipped[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'time',
|
||||
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel])
|
||||
);
|
||||
|
||||
if ($this->testSuiteLevel > 1) {
|
||||
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
|
||||
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel - 1] += $this->testSuiteWarnings[$this->testSuiteLevel];
|
||||
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
|
||||
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
|
||||
}
|
||||
|
||||
$this->testSuiteLevel--;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test started.
|
||||
*
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
$usesDataprovider = false;
|
||||
|
||||
if (method_exists($test, 'usesDataProvider')) {
|
||||
$usesDataprovider = $test->usesDataProvider();
|
||||
}
|
||||
|
||||
$testCase = $this->document->createElement('testcase');
|
||||
$testCase->setAttribute('name', $test->getName());
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($test);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
// @phpstan-ignore-next-line
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$methodName = $test->getName(!$usesDataprovider);
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
try {
|
||||
$method = $class->getMethod($methodName);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
// @phpstan-ignore-next-line
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$testCase->setAttribute('class', $class->getName());
|
||||
$testCase->setAttribute('classname', str_replace('\\', '.', $class->getName()));
|
||||
$fileName = $class->getFileName();
|
||||
if ($fileName !== false) {
|
||||
$testCase->setAttribute('file', $fileName);
|
||||
}
|
||||
$testCase->setAttribute('line', (string) $method->getStartLine());
|
||||
}
|
||||
|
||||
if (TeamCity::isPestTest($test)) {
|
||||
$testCase->setAttribute('class', $test->getPrintableTestCaseName());
|
||||
$testCase->setAttribute('classname', str_replace('\\', '.', $test->getPrintableTestCaseName()));
|
||||
// @phpstan-ignore-next-line
|
||||
$testCase->setAttribute('file', $test->__getFileName());
|
||||
}
|
||||
|
||||
$this->currentTestCase = $testCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test ended.
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
$numAssertions = 0;
|
||||
|
||||
if (method_exists($test, 'getNumAssertions')) {
|
||||
$numAssertions = $test->getNumAssertions();
|
||||
}
|
||||
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
|
||||
|
||||
if ($this->currentTestCase !== null) {
|
||||
$this->currentTestCase->setAttribute(
|
||||
'assertions',
|
||||
(string) $numAssertions
|
||||
);
|
||||
|
||||
$this->currentTestCase->setAttribute(
|
||||
'time',
|
||||
sprintf('%F', $time)
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->appendChild(
|
||||
$this->currentTestCase
|
||||
);
|
||||
}
|
||||
|
||||
$this->testSuiteTests[$this->testSuiteLevel]++;
|
||||
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
|
||||
|
||||
$testOutput = '';
|
||||
|
||||
if (method_exists($test, 'hasOutput') && method_exists($test, 'getActualOutput')) {
|
||||
$testOutput = $test->hasOutput() ? $test->getActualOutput() : '';
|
||||
}
|
||||
|
||||
if ($testOutput !== '') {
|
||||
$systemOut = $this->document->createElement(
|
||||
'system-out',
|
||||
Xml::prepareString($testOutput)
|
||||
);
|
||||
|
||||
if ($this->currentTestCase !== null) {
|
||||
$this->currentTestCase->appendChild($systemOut);
|
||||
}
|
||||
}
|
||||
|
||||
$this->currentTestCase = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XML as a string.
|
||||
*/
|
||||
public function getXML(): string
|
||||
{
|
||||
$xml = $this->document->saveXML();
|
||||
if ($xml === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function doAddFault(Test $test, Throwable $t, string $type): void
|
||||
{
|
||||
if ($this->currentTestCase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($test instanceof SelfDescribing) {
|
||||
$buffer = $test->toString() . "\n";
|
||||
} else {
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
$buffer .= trim(
|
||||
TestFailure::exceptionToString($t) . "\n" .
|
||||
Filter::getFilteredStacktrace($t)
|
||||
);
|
||||
|
||||
$fault = $this->document->createElement(
|
||||
$type,
|
||||
Xml::prepareString($buffer)
|
||||
);
|
||||
|
||||
if ($t instanceof ExceptionWrapper) {
|
||||
$fault->setAttribute('type', $t->getClassName());
|
||||
} else {
|
||||
$fault->setAttribute('type', get_class($t));
|
||||
}
|
||||
|
||||
$this->currentTestCase->appendChild($fault);
|
||||
}
|
||||
|
||||
private function doAddSkipped(): void
|
||||
{
|
||||
if ($this->currentTestCase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$skipped = $this->document->createElement('skipped');
|
||||
|
||||
$this->currentTestCase->appendChild($skipped);
|
||||
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel]++;
|
||||
}
|
||||
// @todo
|
||||
}
|
||||
|
||||
@ -4,294 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Logging;
|
||||
|
||||
use function getmypid;
|
||||
use Pest\Concerns\Logging\WritesToConsole;
|
||||
use Pest\Concerns\Testable;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use function Pest\version;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestResult;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
use function round;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use Throwable;
|
||||
|
||||
final class TeamCity extends DefaultResultPrinter
|
||||
{
|
||||
use WritesToConsole;
|
||||
private const PROTOCOL = 'pest_qn://';
|
||||
private const NAME = 'name';
|
||||
private const LOCATION_HINT = 'locationHint';
|
||||
private const DURATION = 'duration';
|
||||
private const TEST_SUITE_STARTED = 'testSuiteStarted';
|
||||
private const TEST_SUITE_FINISHED = 'testSuiteFinished';
|
||||
private const TEST_COUNT = 'testCount';
|
||||
private const TEST_STARTED = 'testStarted';
|
||||
private const TEST_FINISHED = 'testFinished';
|
||||
|
||||
/** @var int */
|
||||
private $flowId;
|
||||
|
||||
/** @var bool */
|
||||
private $isSummaryTestCountPrinted = false;
|
||||
|
||||
/** @var \PHPUnit\Util\Log\TeamCity */
|
||||
private $phpunitTeamCity;
|
||||
|
||||
/**
|
||||
* @param resource|string|null $out
|
||||
*/
|
||||
public function __construct($out, bool $verbose, string $colors)
|
||||
{
|
||||
parent::__construct($out, $verbose, $colors);
|
||||
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity($out, $verbose, $colors);
|
||||
|
||||
$this->logo();
|
||||
}
|
||||
|
||||
private function logo(): void
|
||||
{
|
||||
$this->writeNewLine();
|
||||
$this->write('Pest ' . version());
|
||||
$this->writeNewLine();
|
||||
}
|
||||
|
||||
public function printResult(TestResult $result): void
|
||||
{
|
||||
$this->write('Tests: ');
|
||||
|
||||
$results = [
|
||||
'failed' => ['count' => $result->errorCount() + $result->failureCount(), 'color' => 'fg-red'],
|
||||
'skipped' => ['count' => $result->skippedCount(), 'color' => 'fg-yellow'],
|
||||
'warned' => ['count' => $result->warningCount(), 'color' => 'fg-yellow'],
|
||||
'risked' => ['count' => $result->riskyCount(), 'color' => 'fg-yellow'],
|
||||
'incomplete' => ['count' => $result->notImplementedCount(), 'color' => 'fg-yellow'],
|
||||
'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'],
|
||||
];
|
||||
|
||||
$filteredResults = array_filter($results, function ($item): bool {
|
||||
return $item['count'] > 0;
|
||||
});
|
||||
|
||||
foreach ($filteredResults as $key => $info) {
|
||||
$this->writeWithColor($info['color'], $info['count'] . " $key", false);
|
||||
|
||||
if ($key !== array_reverse(array_keys($filteredResults))[0]) {
|
||||
$this->write(', ');
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->write("Assertions: $this->numAssertions");
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->write("Time: {$result->time()}s");
|
||||
|
||||
$this->writeNewLine();
|
||||
}
|
||||
|
||||
private function successfulTestCount(TestResult $result): int
|
||||
{
|
||||
return $result->count()
|
||||
- $result->failureCount()
|
||||
- $result->errorCount()
|
||||
- $result->skippedCount()
|
||||
- $result->warningCount()
|
||||
- $result->notImplementedCount()
|
||||
- $result->riskyCount();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
if (static::isCompoundTestSuite($suite)) {
|
||||
$this->writeWithColor('bold', ' ' . $suiteName);
|
||||
} elseif (static::isPestTestSuite($suite)) {
|
||||
$this->writeWithColor('fg-white, bold', ' ' . substr_replace($suiteName, '', 0, 2) . ' ');
|
||||
} else {
|
||||
$this->writeWithColor('fg-white, bold', ' ' . $suiteName);
|
||||
}
|
||||
|
||||
$this->writeNewLine();
|
||||
|
||||
$this->flowId = (int) getmypid();
|
||||
|
||||
if (!$this->isSummaryTestCountPrinted) {
|
||||
$this->printEvent(self::TEST_COUNT, [
|
||||
'count' => $suite->count(),
|
||||
]);
|
||||
$this->isSummaryTestCountPrinted = true;
|
||||
}
|
||||
|
||||
$this->printEvent(self::TEST_SUITE_STARTED, [
|
||||
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|int> $params
|
||||
*/
|
||||
private function printEvent(string $eventName, array $params = []): void
|
||||
{
|
||||
$this->write("##teamcity[{$eventName}");
|
||||
|
||||
if ($this->flowId !== 0) {
|
||||
$params['flowId'] = $this->flowId;
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$escapedValue = self::escapeValue((string) $value);
|
||||
$this->write(" {$key}='{$escapedValue}'");
|
||||
}
|
||||
|
||||
$this->write("]\n");
|
||||
}
|
||||
|
||||
private static function escapeValue(string $text): string
|
||||
{
|
||||
return str_replace(
|
||||
['|', "'", "\n", "\r", ']', '['],
|
||||
['||', "|'", '|n', '|r', '|]', '|['],
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->writeNewLine();
|
||||
|
||||
$this->printEvent(self::TEST_SUITE_FINISHED, [
|
||||
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->startTest($test);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent(self::TEST_STARTED, [
|
||||
self::NAME => $test->getName(),
|
||||
// @phpstan-ignore-next-line
|
||||
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the given test suite is a valid Pest suite.
|
||||
*
|
||||
* @param TestSuite<Test> $suite
|
||||
*/
|
||||
private static function isPestTestSuite(TestSuite $suite): bool
|
||||
{
|
||||
return strncmp($suite->getName(), 'P\\', strlen('P\\')) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test suite is made up of multiple smaller test suites.
|
||||
*
|
||||
* @param TestSuite<Test> $suite
|
||||
*/
|
||||
private static function isCompoundTestSuite(TestSuite $suite): bool
|
||||
{
|
||||
return file_exists($suite->getName()) || !method_exists($suite->getName(), '__getFileName');
|
||||
}
|
||||
|
||||
public static function isPestTest(Test $test): bool
|
||||
{
|
||||
/** @var array<string, string> $uses */
|
||||
$uses = class_uses($test);
|
||||
|
||||
return in_array(Testable::class, $uses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
$this->printEvent(self::TEST_FINISHED, [
|
||||
self::NAME => $test->getName(),
|
||||
self::DURATION => self::toMilliseconds($time),
|
||||
]);
|
||||
|
||||
if (!$this->lastTestFailed) {
|
||||
$this->writeSuccess($test->getName());
|
||||
}
|
||||
|
||||
$this->numAssertions += $test instanceof TestCase ? $test->getNumAssertions() : 1;
|
||||
$this->lastTestFailed = false;
|
||||
}
|
||||
|
||||
private static function toMilliseconds(float $time): int
|
||||
{
|
||||
return (int) round($time * 1000);
|
||||
}
|
||||
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeError($test->getName());
|
||||
$this->phpunitTeamCity->addError($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->markAsFailure($e);
|
||||
$this->writeError($test->getName());
|
||||
$this->phpunitTeamCity->addFailure($test, $e, $time);
|
||||
}
|
||||
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
$this->markAsFailure($e);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addWarning($test, $e, $time);
|
||||
}
|
||||
|
||||
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addIncompleteTest($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addRiskyTest($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time);
|
||||
}
|
||||
|
||||
private function markAsFailure(Throwable $t): void
|
||||
{
|
||||
$this->lastTestFailed = true;
|
||||
ExceptionTrace::removePestReferences($t);
|
||||
}
|
||||
// @todo
|
||||
}
|
||||
|
||||
@ -14,17 +14,12 @@ use SebastianBergmann\Exporter\Exporter;
|
||||
*/
|
||||
final class OppositeExpectation
|
||||
{
|
||||
/**
|
||||
* @var Expectation
|
||||
*/
|
||||
private $original;
|
||||
|
||||
/**
|
||||
* Creates a new opposite expectation.
|
||||
*/
|
||||
public function __construct(Expectation $original)
|
||||
public function __construct(private Expectation $original)
|
||||
{
|
||||
$this->original = $original;
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,7 +32,7 @@ final class OppositeExpectation
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$this->original->toHaveKey($key);
|
||||
} catch (ExpectationFailedException $e) {
|
||||
} catch (ExpectationFailedException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -51,33 +46,34 @@ final class OppositeExpectation
|
||||
* Handle dynamic method calls into the original expectation.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return Expectation|never
|
||||
*/
|
||||
public function __call(string $name, array $arguments): Expectation
|
||||
{
|
||||
try {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->original->{$name}(...$arguments);
|
||||
} catch (ExpectationFailedException $e) {
|
||||
} catch (ExpectationFailedException) {
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$this->throwExpectationFailedException($name, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic properties gets into the original expectation.
|
||||
*
|
||||
* @return Expectation|never
|
||||
*/
|
||||
public function __get(string $name): Expectation
|
||||
{
|
||||
try {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->original->{$name};
|
||||
} catch (ExpectationFailedException $e) {
|
||||
$this->original->{$name}; // @phpstan-ignore-line
|
||||
} catch (ExpectationFailedException) { // @phpstan-ignore-line
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$this->throwExpectationFailedException($name);
|
||||
}
|
||||
|
||||
@ -85,15 +81,15 @@ final class OppositeExpectation
|
||||
* Creates a new expectation failed exception with a nice readable message.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
private function throwExpectationFailedException(string $name, array $arguments = []): void
|
||||
{
|
||||
$exporter = new Exporter();
|
||||
|
||||
$toString = function ($argument) use ($exporter): string {
|
||||
return $exporter->shortenedExport($argument);
|
||||
};
|
||||
$toString = fn ($argument): string => $exporter->shortenedExport($argument);
|
||||
|
||||
throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)), implode(' ', array_map(function ($argument) use ($toString): string { return $toString($argument); }, $arguments))));
|
||||
throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)), implode(' ', array_map(fn ($argument): string => $toString($argument), $arguments))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\PendingObjects;
|
||||
namespace Pest\PendingCalls;
|
||||
|
||||
use Closure;
|
||||
use Pest\Support\Backtrace;
|
||||
@ -17,47 +17,30 @@ use Pest\TestSuite;
|
||||
final class AfterEachCall
|
||||
{
|
||||
/**
|
||||
* Holds the test suite.
|
||||
*
|
||||
* @var TestSuite
|
||||
* The "afterEach" closure.
|
||||
*/
|
||||
private $testSuite;
|
||||
private Closure $closure;
|
||||
|
||||
/**
|
||||
* Holds the filename.
|
||||
*
|
||||
* @var string
|
||||
* The calls that should be proxied.
|
||||
*/
|
||||
private $filename;
|
||||
private HigherOrderMessageCollection $proxies;
|
||||
|
||||
/**
|
||||
* Holds the before each closure.
|
||||
*
|
||||
* @var Closure
|
||||
* Creates a new Pending Call.
|
||||
*/
|
||||
private $closure;
|
||||
|
||||
/**
|
||||
* Holds calls that should be proxied.
|
||||
*
|
||||
* @var HigherOrderMessageCollection
|
||||
*/
|
||||
private $proxies;
|
||||
|
||||
/**
|
||||
* Creates a new instance of before each call.
|
||||
*/
|
||||
public function __construct(TestSuite $testSuite, string $filename, Closure $closure = null)
|
||||
{
|
||||
$this->testSuite = $testSuite;
|
||||
$this->filename = $filename;
|
||||
public function __construct(
|
||||
private TestSuite $testSuite,
|
||||
private string $filename,
|
||||
Closure $closure = null
|
||||
) {
|
||||
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
||||
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the creation of each call.
|
||||
* Creates the Call.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\PendingObjects;
|
||||
namespace Pest\PendingCalls;
|
||||
|
||||
use Closure;
|
||||
use Pest\Support\Backtrace;
|
||||
@ -16,48 +16,31 @@ use Pest\TestSuite;
|
||||
*/
|
||||
final class BeforeEachCall
|
||||
{
|
||||
/**
|
||||
* Holds the test suite.
|
||||
*
|
||||
* @var TestSuite
|
||||
*/
|
||||
private $testSuite;
|
||||
|
||||
/**
|
||||
* Holds the filename.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $filename;
|
||||
|
||||
/**
|
||||
* Holds the before each closure.
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
private $closure;
|
||||
private \Closure $closure;
|
||||
|
||||
/**
|
||||
* Holds calls that should be proxied.
|
||||
*
|
||||
* @var HigherOrderMessageCollection
|
||||
* The calls that should be proxied.
|
||||
*/
|
||||
private $proxies;
|
||||
private HigherOrderMessageCollection $proxies;
|
||||
|
||||
/**
|
||||
* Creates a new instance of before each call.
|
||||
* Creates a new Pending Call.
|
||||
*/
|
||||
public function __construct(TestSuite $testSuite, string $filename, Closure $closure = null)
|
||||
{
|
||||
$this->testSuite = $testSuite;
|
||||
$this->filename = $filename;
|
||||
public function __construct(
|
||||
private TestSuite $testSuite,
|
||||
private string $filename,
|
||||
Closure $closure = null
|
||||
) {
|
||||
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
|
||||
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the creation of each call.
|
||||
* Creates the Call.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
@ -2,10 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\PendingObjects;
|
||||
namespace Pest\PendingCalls;
|
||||
|
||||
use Closure;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\HigherOrderCallables;
|
||||
use Pest\Support\NullClosure;
|
||||
@ -20,39 +20,25 @@ use SebastianBergmann\Exporter\Exporter;
|
||||
final class TestCall
|
||||
{
|
||||
/**
|
||||
* Holds the test suite.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var TestSuite
|
||||
* The Test Case Factory.
|
||||
*/
|
||||
private $testSuite;
|
||||
|
||||
/**
|
||||
* Holds the test case factory.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var TestCaseFactory
|
||||
*/
|
||||
private $testCaseFactory;
|
||||
private TestCaseMethodFactory $testCaseMethod;
|
||||
|
||||
/**
|
||||
* If test call is descriptionLess.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $descriptionLess = false;
|
||||
private bool $descriptionLess;
|
||||
|
||||
/**
|
||||
* Creates a new instance of a pending test call.
|
||||
* Creates a new Pending Call.
|
||||
*/
|
||||
public function __construct(TestSuite $testSuite, string $filename, string $description = null, Closure $closure = null)
|
||||
{
|
||||
$this->testCaseFactory = new TestCaseFactory($filename, $description, $closure);
|
||||
$this->testSuite = $testSuite;
|
||||
public function __construct(
|
||||
private TestSuite $testSuite,
|
||||
string $filename,
|
||||
string $description = null,
|
||||
Closure $closure = null
|
||||
) {
|
||||
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
|
||||
$this->descriptionLess = $description === null;
|
||||
}
|
||||
|
||||
@ -62,7 +48,7 @@ final class TestCall
|
||||
public function throws(string $exception, string $exceptionMessage = null): TestCall
|
||||
{
|
||||
if (class_exists($exception)) {
|
||||
$this->testCaseFactory
|
||||
$this->testCaseMethod
|
||||
->proxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]);
|
||||
} else {
|
||||
@ -70,7 +56,7 @@ final class TestCall
|
||||
}
|
||||
|
||||
if (is_string($exceptionMessage)) {
|
||||
$this->testCaseFactory
|
||||
$this->testCaseMethod
|
||||
->proxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]);
|
||||
}
|
||||
@ -83,12 +69,12 @@ final class TestCall
|
||||
*
|
||||
* @param (callable(): bool)|bool $condition
|
||||
*/
|
||||
public function throwsIf($condition, string $exception, string $exceptionMessage = null): TestCall
|
||||
public function throwsIf(callable|bool $condition, string $exception, string $exceptionMessage = null): TestCall
|
||||
{
|
||||
$condition = is_callable($condition)
|
||||
? $condition
|
||||
: static function () use ($condition): bool {
|
||||
return (bool) $condition; // @phpstan-ignore-line
|
||||
return $condition;
|
||||
};
|
||||
|
||||
if ($condition()) {
|
||||
@ -104,10 +90,10 @@ final class TestCall
|
||||
*
|
||||
* @param array<\Closure|iterable<int|string, mixed>|string> $data
|
||||
*/
|
||||
public function with(...$data): TestCall
|
||||
public function with(Closure|iterable|string ...$data): TestCall
|
||||
{
|
||||
foreach ($data as $dataset) {
|
||||
$this->testCaseFactory->datasets[] = $dataset;
|
||||
$this->testCaseMethod->datasets[] = $dataset;
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -116,11 +102,11 @@ final class TestCall
|
||||
/**
|
||||
* Sets the test depends.
|
||||
*/
|
||||
public function depends(string ...$tests): TestCall
|
||||
public function depends(string ...$depends): TestCall
|
||||
{
|
||||
$this->testCaseFactory
|
||||
->factoryProxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'addDependencies', [$tests]);
|
||||
foreach ($depends as $depend) {
|
||||
$this->testCaseMethod->depends[] = $depend;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -130,7 +116,7 @@ final class TestCall
|
||||
*/
|
||||
public function only(): TestCall
|
||||
{
|
||||
$this->testCaseFactory->only = true;
|
||||
$this->testCaseMethod->only = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -140,19 +126,17 @@ final class TestCall
|
||||
*/
|
||||
public function group(string ...$groups): TestCall
|
||||
{
|
||||
$this->testCaseFactory
|
||||
->factoryProxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'addGroups', [$groups]);
|
||||
foreach ($groups as $group) {
|
||||
$this->testCaseMethod->groups[] = $group;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the current test.
|
||||
*
|
||||
* @param Closure|bool|string $conditionOrMessage
|
||||
*/
|
||||
public function skip($conditionOrMessage = true, string $message = ''): TestCall
|
||||
public function skip(Closure|bool|string $conditionOrMessage = true, string $message = ''): TestCall
|
||||
{
|
||||
$condition = is_string($conditionOrMessage)
|
||||
? NullClosure::create()
|
||||
@ -160,9 +144,7 @@ final class TestCall
|
||||
|
||||
$condition = is_callable($condition)
|
||||
? $condition
|
||||
: function () use ($condition) { /* @phpstan-ignore-line */
|
||||
return $condition;
|
||||
};
|
||||
: fn () => $condition;
|
||||
|
||||
$message = is_string($conditionOrMessage)
|
||||
? $conditionOrMessage
|
||||
@ -171,7 +153,7 @@ final class TestCall
|
||||
/** @var callable(): bool $condition */
|
||||
$condition = $condition->bindTo(null);
|
||||
|
||||
$this->testCaseFactory
|
||||
$this->testCaseMethod
|
||||
->chains
|
||||
->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
|
||||
|
||||
@ -203,16 +185,16 @@ final class TestCall
|
||||
*/
|
||||
private function addChain(string $name, array $arguments = null): self
|
||||
{
|
||||
$this->testCaseFactory
|
||||
$this->testCaseMethod
|
||||
->chains
|
||||
->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
|
||||
|
||||
if ($this->descriptionLess) {
|
||||
$exporter = new Exporter();
|
||||
if ($this->testCaseFactory->description !== null) {
|
||||
$this->testCaseFactory->description .= ' → ';
|
||||
if ($this->testCaseMethod->description !== null) {
|
||||
$this->testCaseMethod->description .= ' → ';
|
||||
}
|
||||
$this->testCaseFactory->description .= $arguments === null
|
||||
$this->testCaseMethod->description .= $arguments === null
|
||||
? $name
|
||||
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
|
||||
}
|
||||
@ -221,11 +203,10 @@ final class TestCall
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the current test case factory
|
||||
* to the tests repository.
|
||||
* Creates the Call.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->testSuite->tests->set($this->testCaseFactory);
|
||||
$this->testSuite->tests->set($this->testCaseMethod);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\PendingObjects;
|
||||
namespace Pest\PendingCalls;
|
||||
|
||||
use Closure;
|
||||
use Pest\TestSuite;
|
||||
@ -24,45 +24,31 @@ final class UsesCall
|
||||
*
|
||||
* @var array<int, Closure>
|
||||
*/
|
||||
private $hooks = [];
|
||||
|
||||
/**
|
||||
* Holds the class and traits.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $classAndTraits;
|
||||
|
||||
/**
|
||||
* Holds the base dirname here the uses call was performed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $filename;
|
||||
private array $hooks = [];
|
||||
|
||||
/**
|
||||
* Holds the targets of the uses.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $targets;
|
||||
private array $targets;
|
||||
|
||||
/**
|
||||
* Holds the groups of the uses.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private $groups = [];
|
||||
private array $groups = [];
|
||||
|
||||
/**
|
||||
* Creates a new instance of a pending test uses.
|
||||
* Creates a new Pending Call.
|
||||
*
|
||||
* @param array<int, string> $classAndTraits
|
||||
*/
|
||||
public function __construct(string $filename, array $classAndTraits)
|
||||
{
|
||||
$this->classAndTraits = $classAndTraits;
|
||||
$this->filename = $filename;
|
||||
public function __construct(
|
||||
private string $filename,
|
||||
private array $classAndTraits
|
||||
) {
|
||||
$this->targets = [$filename];
|
||||
}
|
||||
|
||||
@ -76,14 +62,12 @@ final class UsesCall
|
||||
$startChar = DIRECTORY_SEPARATOR;
|
||||
|
||||
if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) {
|
||||
$path = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', function ($match): string {
|
||||
return strtolower($match['drive']);
|
||||
}, $path);
|
||||
$path = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', fn ($match): string => strtolower($match['drive']), $path);
|
||||
|
||||
$startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__));
|
||||
}
|
||||
|
||||
return 0 === strpos($path, $startChar)
|
||||
return str_starts_with($path, $startChar)
|
||||
? $path
|
||||
: implode(DIRECTORY_SEPARATOR, [
|
||||
dirname($this->filename),
|
||||
@ -151,7 +135,7 @@ final class UsesCall
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the creation of uses.
|
||||
* Creates the Call.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
@ -6,7 +6,7 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '1.20.0';
|
||||
return '2.x-dev';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
|
||||
@ -14,7 +14,7 @@ final class Plugin
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static $callables = [];
|
||||
public static array $callables = [];
|
||||
|
||||
/**
|
||||
* Lazy loads an `uses` call on the context of plugins.
|
||||
|
||||
31
src/Plugins/Actions/AddsOutput.php
Normal file
31
src/Plugins/Actions/AddsOutput.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Actions;
|
||||
|
||||
use Pest\Contracts\Plugins;
|
||||
use Pest\Plugin\Loader;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AddsOutput
|
||||
{
|
||||
/**
|
||||
* Executes the Plugin action.
|
||||
*
|
||||
* Provides an opportunity for any plugins that want to provide additional output after test execution.
|
||||
*/
|
||||
public function __invoke(int $exitCode): int
|
||||
{
|
||||
$plugins = Loader::getPlugins(Plugins\AddsOutput::class);
|
||||
|
||||
/** @var Plugins\AddsOutput $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$exitCode = $plugin->addOutput($exitCode);
|
||||
}
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
}
|
||||
35
src/Plugins/Actions/HandleArguments.php
Normal file
35
src/Plugins/Actions/HandleArguments.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Actions;
|
||||
|
||||
use Pest\Contracts\Plugins;
|
||||
use Pest\Plugin\Loader;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class HandleArguments
|
||||
{
|
||||
/**
|
||||
* Executes the Plugin action.
|
||||
*
|
||||
* Transform the input arguments by passing it to the relevant plugins.
|
||||
*
|
||||
* @param array<int, string> $argv
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function __invoke(array $argv): array
|
||||
{
|
||||
$plugins = Loader::getPlugins(Plugins\HandlesArguments::class);
|
||||
|
||||
/** @var Plugins\HandlesArguments $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$argv = $plugin->handleArguments($argv);
|
||||
}
|
||||
|
||||
return $argv;
|
||||
}
|
||||
}
|
||||
@ -28,32 +28,31 @@ final class Coverage implements AddsOutput, HandlesArguments
|
||||
private const MIN_OPTION = 'min';
|
||||
|
||||
/**
|
||||
* Whether should show the coverage or not.
|
||||
*
|
||||
* @var bool
|
||||
* Whether it should show the coverage or not.
|
||||
*/
|
||||
public $coverage = false;
|
||||
public bool $coverage = false;
|
||||
|
||||
/**
|
||||
* The minimum coverage.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
public $coverageMin = 0.0;
|
||||
public float $coverageMin = 0.0;
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
* Creates a new Plugin instance.
|
||||
*/
|
||||
private $output;
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
public function __construct(private OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $originals
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function handleArguments(array $originals): array
|
||||
{
|
||||
$arguments = array_merge([''], array_values(array_filter($originals, function ($original): bool {
|
||||
$arguments = [...[''], ...array_values(array_filter($originals, function ($original): bool {
|
||||
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
|
||||
if ($original === sprintf('--%s', $option) || Str::startsWith($original, sprintf('--%s=', $option))) {
|
||||
return true;
|
||||
@ -61,7 +60,7 @@ final class Coverage implements AddsOutput, HandlesArguments
|
||||
}
|
||||
|
||||
return false;
|
||||
})));
|
||||
}))];
|
||||
|
||||
$originals = array_flip($originals);
|
||||
foreach ($arguments as $argument) {
|
||||
|
||||
@ -21,17 +21,10 @@ final class Environment implements HandlesArguments
|
||||
*/
|
||||
public const LOCAL = 'local';
|
||||
|
||||
/**
|
||||
* @var \Pest\Plugins\Environment|null
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* The current environment.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private static $name;
|
||||
private static ?string $name = null;
|
||||
|
||||
/**
|
||||
* Allows to handle custom command line arguments.
|
||||
|
||||
@ -28,23 +28,14 @@ final class Init implements HandlesArguments
|
||||
'ExampleTest.php' => 'tests/ExampleTest.php',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* @var TestSuite
|
||||
*/
|
||||
private $testSuite;
|
||||
|
||||
/**
|
||||
* Creates a new Plugin instance.
|
||||
*/
|
||||
public function __construct(TestSuite $testSuite, OutputInterface $output)
|
||||
{
|
||||
$this->testSuite = $testSuite;
|
||||
$this->output = $output;
|
||||
public function __construct(
|
||||
private TestSuite $testSuite,
|
||||
private OutputInterface $output
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
public function handleArguments(array $arguments): array
|
||||
|
||||
@ -13,17 +13,13 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
final class Version implements HandlesArguments
|
||||
{
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the plugin.
|
||||
*/
|
||||
public function __construct(OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
public function __construct(
|
||||
private OutputInterface $output
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
public function handleArguments(array $arguments): array
|
||||
|
||||
@ -17,7 +17,7 @@ final class AfterAllRepository
|
||||
/**
|
||||
* @var array<string, Closure>
|
||||
*/
|
||||
private $state = [];
|
||||
private array $state = [];
|
||||
|
||||
/**
|
||||
* Runs the given closure for each after all.
|
||||
|
||||
@ -18,7 +18,7 @@ final class AfterEachRepository
|
||||
/**
|
||||
* @var array<string, Closure>
|
||||
*/
|
||||
private $state = [];
|
||||
private array $state = [];
|
||||
|
||||
/**
|
||||
* Sets a after each closure.
|
||||
|
||||
@ -17,7 +17,7 @@ final class BeforeAllRepository
|
||||
/**
|
||||
* @var array<string, Closure>
|
||||
*/
|
||||
private $state = [];
|
||||
private array $state = [];
|
||||
|
||||
/**
|
||||
* Runs one before all closure, and unsets it from the repository.
|
||||
|
||||
@ -16,7 +16,7 @@ final class BeforeEachRepository
|
||||
/**
|
||||
* @var array<string, Closure>
|
||||
*/
|
||||
private $state = [];
|
||||
private array $state = [];
|
||||
|
||||
/**
|
||||
* Sets a before each closure.
|
||||
|
||||
@ -5,16 +5,11 @@ declare(strict_types=1);
|
||||
namespace Pest\Repositories;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\DatasetMissing;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Exceptions\TestAlreadyExist;
|
||||
use Pest\Exceptions\TestCaseAlreadyInUse;
|
||||
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Plugins\Environment;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Support\Str;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@ -22,27 +17,22 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
final class TestRepository
|
||||
{
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private const SEPARATOR = '>>>';
|
||||
|
||||
/**
|
||||
* @var array<string, TestCaseFactory>
|
||||
*/
|
||||
private $state = [];
|
||||
private array $testCases = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<int, array<int, string|Closure>>>
|
||||
*/
|
||||
private $uses = [];
|
||||
private array $uses = [];
|
||||
|
||||
/**
|
||||
* Counts the number of test cases.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->state);
|
||||
return count($this->testCases);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,81 +42,13 @@ final class TestRepository
|
||||
*/
|
||||
public function getFilenames(): array
|
||||
{
|
||||
$testsWithOnly = $this->testsUsingOnly();
|
||||
$testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase) => count($testCase->methodsUsingOnly()) > 0);
|
||||
|
||||
return array_values(array_map(function (TestCaseFactory $factory): string {
|
||||
return $factory->filename;
|
||||
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
|
||||
if (count($testCases) === 0) {
|
||||
$testCases = $this->testCases;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, $hooks] = $uses;
|
||||
|
||||
$setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith, $hooks): void {
|
||||
[$filename] = explode(self::SEPARATOR, $key);
|
||||
|
||||
if ((!is_dir($path) && $filename === $path) || (is_dir($path) && $startsWith($filename, $path))) {
|
||||
foreach ($classOrTraits as $class) { /** @var string $class */
|
||||
if (class_exists($class)) {
|
||||
if ($testCase->class !== TestCase::class) {
|
||||
throw new TestCaseAlreadyInUse($testCase->class, $class, $filename);
|
||||
}
|
||||
$testCase->class = $class;
|
||||
} elseif (trait_exists($class)) {
|
||||
$testCase->traits[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
// IDEA: Consider set the real lines on these.
|
||||
$testCase->factoryProxies->add($filename, 0, 'addGroups', [$groups]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addBeforeAll', [$hooks[0] ?? null]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addBeforeEach', [$hooks[1] ?? null]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addAfterEach', [$hooks[2] ?? null]);
|
||||
$testCase->factoryProxies->add($filename, 0, 'addAfterAll', [$hooks[3] ?? null]);
|
||||
}
|
||||
};
|
||||
|
||||
foreach ($this->state as $key => $test) {
|
||||
$setClassName($test, $key);
|
||||
}
|
||||
}
|
||||
|
||||
$onlyState = $this->testsUsingOnly();
|
||||
|
||||
$state = count($onlyState) > 0 ? $onlyState : $this->state;
|
||||
|
||||
foreach ($state as $testFactory) {
|
||||
/** @var TestCaseFactory $testFactory */
|
||||
$tests = $testFactory->build($testSuite);
|
||||
foreach ($tests as $test) {
|
||||
$each($test);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all tests that have called the only method.
|
||||
*
|
||||
* @return array<TestCaseFactory>
|
||||
*/
|
||||
private function testsUsingOnly(): array
|
||||
{
|
||||
if (Environment::name() === Environment::CI) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($this->state, function ($testFactory): bool {
|
||||
return $testFactory->only;
|
||||
});
|
||||
return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $testCases));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,8 +70,8 @@ final class TestRepository
|
||||
foreach ($paths as $path) {
|
||||
if (array_key_exists($path, $this->uses)) {
|
||||
$this->uses[$path] = [
|
||||
array_merge($this->uses[$path][0], $classOrTraits),
|
||||
array_merge($this->uses[$path][1], $groups),
|
||||
[...$this->uses[$path][0], ...$classOrTraits],
|
||||
[...$this->uses[$path][1], ...$groups],
|
||||
$this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices
|
||||
];
|
||||
} else {
|
||||
@ -158,27 +80,73 @@ final class TestRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a test case by the given filename and description.
|
||||
*/
|
||||
public function set(TestCaseFactory $test): void
|
||||
public function get($filename): TestCaseFactory
|
||||
{
|
||||
if ($test->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('Trying to create a test without description.');
|
||||
return $this->testCases[$filename];
|
||||
}
|
||||
|
||||
if (array_key_exists(sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description), $this->state)) {
|
||||
throw new TestAlreadyExist($test->filename, $test->description);
|
||||
/**
|
||||
* Sets a new test case method.
|
||||
*/
|
||||
public function set(TestCaseMethodFactory $method): void
|
||||
{
|
||||
if (!array_key_exists($method->filename, $this->testCases)) {
|
||||
$this->testCases[$method->filename] = new TestCaseFactory($method->filename);
|
||||
}
|
||||
|
||||
if (!$test->receivesArguments()) {
|
||||
$arguments = Reflection::getFunctionArguments($test->test);
|
||||
$this->testCases[$method->filename]->addMethod($method);
|
||||
}
|
||||
|
||||
if (count($arguments) > 0) {
|
||||
throw new DatasetMissing($test->filename, $test->description, $arguments);
|
||||
/**
|
||||
* Makes a Test Case from the given filename, if exists.
|
||||
*/
|
||||
public function makeIfExists(string $filename): void
|
||||
{
|
||||
if (array_key_exists($filename, $this->testCases)) {
|
||||
$this->make($this->testCases[$filename]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test;
|
||||
/**
|
||||
* Makes a Test Case using the given factory.
|
||||
*/
|
||||
private function make(TestCaseFactory $testCase): void
|
||||
{
|
||||
$startsWith = static fn (string $target, string $directory): bool => Str::startsWith($target, $directory . DIRECTORY_SEPARATOR);
|
||||
|
||||
foreach ($this->uses as $path => $uses) {
|
||||
[$classOrTraits, $groups, $hooks] = $uses;
|
||||
|
||||
if ((!is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) {
|
||||
foreach ($classOrTraits as $class) {
|
||||
/** @var string $class */
|
||||
if (class_exists($class)) {
|
||||
if ($testCase->class !== TestCase::class) {
|
||||
throw new TestCaseAlreadyInUse($testCase->class, $class, $testCase->filename);
|
||||
}
|
||||
$testCase->class = $class;
|
||||
} elseif (trait_exists($class)) {
|
||||
$testCase->traits[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($testCase->methods as $method) {
|
||||
foreach ($groups as $group) {
|
||||
$method->groups[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($testCase->methods as $method) {
|
||||
$method->groups = array_merge($groups, $method->groups);
|
||||
}
|
||||
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeAll', [$hooks[0] ?? null]);
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeEach', [$hooks[1] ?? null]);
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterEach', [$hooks[2] ?? null]);
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterAll', [$hooks[3] ?? null]);
|
||||
}
|
||||
}
|
||||
|
||||
$testCase->make();
|
||||
}
|
||||
}
|
||||
|
||||
22
src/Subscribers/EnsureConfigurationDefaults.php
Normal file
22
src/Subscribers/EnsureConfigurationDefaults.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use PHPUnit\Event\TestRunner\Configured;
|
||||
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureConfigurationDefaults implements ConfiguredSubscriber
|
||||
{
|
||||
/**
|
||||
* Runs the subscriber.
|
||||
*/
|
||||
public function notify(Configured $event): void
|
||||
{
|
||||
// TODO...
|
||||
}
|
||||
}
|
||||
27
src/Subscribers/EnsureConfigurationIsValid.php
Normal file
27
src/Subscribers/EnsureConfigurationIsValid.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Subscribers;
|
||||
|
||||
use Pest\Exceptions\AttributeNotSupportedYet;
|
||||
use PHPUnit\Event\TestRunner\Configured;
|
||||
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EnsureConfigurationIsValid implements ConfiguredSubscriber
|
||||
{
|
||||
/**
|
||||
* Runs the subscriber.
|
||||
*/
|
||||
public function notify(Configured $event): void
|
||||
{
|
||||
$configuration = $event->configuration();
|
||||
|
||||
if ($configuration->processIsolation()) {
|
||||
throw new AttributeNotSupportedYet('processIsolation', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,19 +5,14 @@ declare(strict_types=1);
|
||||
namespace Pest\Support;
|
||||
|
||||
/**
|
||||
* Credits: most of this class methods and implementations
|
||||
* belongs to the Arr helper of laravel/framework project
|
||||
* (https://github.com/laravel/framework).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Arr
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $array
|
||||
* @param string|int $key
|
||||
* Checks if the given array has the given key.
|
||||
*/
|
||||
public static function has(array $array, $key): bool
|
||||
public static function has(array $array, string|int $key): bool
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
@ -37,13 +32,9 @@ final class Arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $array
|
||||
* @param string|int $key
|
||||
* @param null $default
|
||||
*
|
||||
* @return array|mixed|null
|
||||
* Gets the given key value.
|
||||
*/
|
||||
public static function get(array $array, $key, $default = null)
|
||||
public static function get(array $array, string|int $key, mixed $default = null): mixed
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
@ -51,7 +42,7 @@ final class Arr
|
||||
return $array[$key];
|
||||
}
|
||||
|
||||
if (strpos($key, '.') === false) {
|
||||
if (!str_contains($key, '.')) {
|
||||
return $array[$key] ?? $default;
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ final class Backtrace
|
||||
$current = null;
|
||||
|
||||
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
|
||||
if (Str::endsWith($trace[self::FILE], (string) realpath('vendor/phpunit/phpunit/src/Util/FileLoader.php'))) {
|
||||
if (Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -17,10 +18,12 @@ final class ChainableClosure
|
||||
public static function from(Closure $closure, Closure $next): Closure
|
||||
{
|
||||
return function () use ($closure, $next): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($closure, $this, get_class($this)), func_get_args());
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($next, $this, get_class($this)), func_get_args());
|
||||
if (!is_object($this)) { // @phpstan-ignore-line
|
||||
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
|
||||
}
|
||||
|
||||
call_user_func_array(Closure::bind($closure, $this, $this::class), func_get_args());
|
||||
call_user_func_array(Closure::bind($next, $this, $this::class), func_get_args());
|
||||
};
|
||||
}
|
||||
|
||||
@ -30,9 +33,7 @@ final class ChainableClosure
|
||||
public static function fromStatic(Closure $closure, Closure $next): Closure
|
||||
{
|
||||
return static function () use ($closure, $next): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($closure, null, self::class), func_get_args());
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($next, null, self::class), func_get_args());
|
||||
};
|
||||
}
|
||||
|
||||
@ -13,15 +13,12 @@ use ReflectionParameter;
|
||||
*/
|
||||
final class Container
|
||||
{
|
||||
/**
|
||||
* @var self
|
||||
*/
|
||||
private static $instance;
|
||||
private static ?Container $instance = null;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private $instances = [];
|
||||
private array $instances = [];
|
||||
|
||||
/**
|
||||
* Gets a new or already existing container.
|
||||
@ -66,7 +63,6 @@ final class Container
|
||||
*/
|
||||
private function build(string $id): object
|
||||
{
|
||||
/** @phpstan-ignore-next-line */
|
||||
$reflectionClass = new ReflectionClass($id);
|
||||
|
||||
if ($reflectionClass->isInstantiable()) {
|
||||
|
||||
@ -30,14 +30,21 @@ final class Coverage
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs true there is any code
|
||||
* coverage driver available.
|
||||
* Runs true there is any code coverage driver available.
|
||||
*/
|
||||
public static function isAvailable(): bool
|
||||
{
|
||||
return (new Runtime())->canCollectCodeCoverage();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is using Xdebug.
|
||||
*/
|
||||
public static function usingXdebug(): bool
|
||||
{
|
||||
return (new Runtime())->hasXdebug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the code coverage report to the
|
||||
* console and returns the result in float.
|
||||
@ -45,6 +52,14 @@ final class Coverage
|
||||
public static function report(OutputInterface $output): float
|
||||
{
|
||||
if (!file_exists($reportPath = self::getPath())) {
|
||||
if (self::usingXdebug()) {
|
||||
$output->writeln(
|
||||
" <fg=black;bg=yellow;options=bold> WARN </> Unable to get coverage using Xdebug. Did you set <href=https://xdebug.org/docs/code_coverage#mode>Xdebug's coverage mode</>?</>",
|
||||
);
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath));
|
||||
}
|
||||
|
||||
@ -147,7 +162,7 @@ final class Coverage
|
||||
|
||||
$lastKey = count($array) - 1;
|
||||
|
||||
if (array_key_exists($lastKey, $array) && strpos($array[$lastKey], '..') !== false) {
|
||||
if (array_key_exists($lastKey, $array) && str_contains($array[$lastKey], '..')) {
|
||||
[$from] = explode('..', $array[$lastKey]);
|
||||
$array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);
|
||||
|
||||
|
||||
@ -8,19 +8,13 @@ use Closure;
|
||||
|
||||
final class Extendable
|
||||
{
|
||||
/**
|
||||
* The extendable class.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $extendableClass;
|
||||
|
||||
/**
|
||||
* Creates a new extendable instance.
|
||||
*/
|
||||
public function __construct(string $extendableClass)
|
||||
{
|
||||
$this->extendableClass = $extendableClass;
|
||||
public function __construct(
|
||||
private string $extendableClass
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -6,8 +6,6 @@ namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
use Pest\Expectation;
|
||||
use Pest\PendingObjects\TestCall;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -15,13 +13,11 @@ use PHPUnit\Framework\TestCase;
|
||||
final class HigherOrderCallables
|
||||
{
|
||||
/**
|
||||
* @var object
|
||||
* Creates a new Higher Order Callables instances.
|
||||
*/
|
||||
private $target;
|
||||
|
||||
public function __construct(object $target)
|
||||
public function __construct(private object $target)
|
||||
{
|
||||
$this->target = $target;
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,11 +25,11 @@ final class HigherOrderCallables
|
||||
*
|
||||
* Create a new expectation. Callable values will be executed prior to returning the new expectation.
|
||||
*
|
||||
* @param callable|TValue $value
|
||||
* @param (callable():TValue)|TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function expect($value)
|
||||
public function expect(mixed $value): Expectation
|
||||
{
|
||||
return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value);
|
||||
}
|
||||
@ -47,17 +43,15 @@ final class HigherOrderCallables
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function and($value)
|
||||
public function and(mixed $value)
|
||||
{
|
||||
return $this->expect($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tap into the test case to perform an action and return the test case.
|
||||
*
|
||||
* @return TestCall|TestCase|object
|
||||
*/
|
||||
public function tap(callable $callable)
|
||||
public function tap(callable $callable): object
|
||||
{
|
||||
Reflection::bindCallableWithData($callable);
|
||||
|
||||
|
||||
@ -15,70 +15,32 @@ final class HigherOrderMessage
|
||||
{
|
||||
public const UNDEFINED_METHOD = 'Method %s does not exist';
|
||||
|
||||
/**
|
||||
* The filename where the function was originally called.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $filename;
|
||||
|
||||
/**
|
||||
* The line where the function was originally called.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* The method or property name to access.
|
||||
*
|
||||
* @readonly
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The arguments.
|
||||
*
|
||||
* @var array<int, mixed>|null
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
public $arguments;
|
||||
|
||||
/**
|
||||
* An optional condition that will determine if the message will be executed.
|
||||
*
|
||||
* @var callable(): bool|null
|
||||
* @var (Closure(): bool)|null
|
||||
*/
|
||||
public $condition = null;
|
||||
public ?Closure $condition = null;
|
||||
|
||||
/**
|
||||
* Creates a new higher order message.
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __construct(string $filename, int $line, string $methodName, $arguments)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->line = $line;
|
||||
$this->name = $methodName;
|
||||
$this->arguments = $arguments;
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
public int $line,
|
||||
public string $name,
|
||||
public ?array $arguments
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-throws the given `$throwable` with the good line and filename.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(object $target)
|
||||
public function call(object $target): mixed
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) {
|
||||
return $target;
|
||||
}
|
||||
@ -122,10 +84,8 @@ final class HigherOrderMessage
|
||||
|
||||
/**
|
||||
* Determines whether or not there exists a higher order callable with the message name.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function hasHigherOrderCallable()
|
||||
private function hasHigherOrderCallable(): bool
|
||||
{
|
||||
return in_array($this->name, get_class_methods(HigherOrderCallables::class), true);
|
||||
}
|
||||
@ -133,7 +93,7 @@ final class HigherOrderMessage
|
||||
private static function getUndefinedMethodMessage(object $target, string $methodName): string
|
||||
{
|
||||
if (\PHP_MAJOR_VERSION >= 8) {
|
||||
return sprintf(sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', get_class($target), $methodName)));
|
||||
return sprintf(sprintf(self::UNDEFINED_METHOD, sprintf('%s::%s()', $target::class, $methodName)));
|
||||
}
|
||||
|
||||
return sprintf(self::UNDEFINED_METHOD, $methodName);
|
||||
|
||||
@ -12,14 +12,14 @@ final class HigherOrderMessageCollection
|
||||
/**
|
||||
* @var array<int, HigherOrderMessage>
|
||||
*/
|
||||
private $messages = [];
|
||||
private array $messages = [];
|
||||
|
||||
/**
|
||||
* Adds a new higher order message to the collection.
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function add(string $filename, int $line, string $name, array $arguments = null): void
|
||||
public function add(string $filename, int $line, string $name, ?array $arguments): void
|
||||
{
|
||||
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments);
|
||||
}
|
||||
@ -29,7 +29,7 @@ final class HigherOrderMessageCollection
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function addWhen(callable $condition, string $filename, int $line, string $name, array $arguments = null): void
|
||||
public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void
|
||||
{
|
||||
$this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
|
||||
}
|
||||
@ -63,9 +63,7 @@ final class HigherOrderMessageCollection
|
||||
{
|
||||
return array_reduce(
|
||||
$this->messages,
|
||||
static function (int $total, HigherOrderMessage $message) use ($name): int {
|
||||
return $total + (int) ($name === $message->name);
|
||||
},
|
||||
static fn (int $total, HigherOrderMessage $message): int => $total + (int) ($name === $message->name),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,21 +13,15 @@ use Throwable;
|
||||
*/
|
||||
final class HigherOrderTapProxy
|
||||
{
|
||||
private const UNDEFINED_PROPERTY = 'Undefined property: P\\';
|
||||
|
||||
/**
|
||||
* The target being tapped.
|
||||
*
|
||||
* @var TestCase
|
||||
*/
|
||||
public $target;
|
||||
private const UNDEFINED_PROPERTY = 'Undefined property: P\\'; // @phpstan-ignore-line
|
||||
|
||||
/**
|
||||
* Create a new tap proxy instance.
|
||||
*/
|
||||
public function __construct(TestCase $target)
|
||||
{
|
||||
$this->target = $target;
|
||||
public function __construct(
|
||||
public TestCase $target
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,8 +31,7 @@ final class HigherOrderTapProxy
|
||||
*/
|
||||
public function __set(string $property, $value): void
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
$this->target->{$property} = $value;
|
||||
$this->target->{$property} = $value; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,9 +42,8 @@ final class HigherOrderTapProxy
|
||||
public function __get(string $property)
|
||||
{
|
||||
try {
|
||||
// @phpstan-ignore-next-line
|
||||
return $this->target->{$property};
|
||||
} catch (Throwable $throwable) {
|
||||
return $this->target->{$property}; // @phpstan-ignore-line
|
||||
} catch (Throwable $throwable) { // @phpstan-ignore-line
|
||||
Reflection::setPropertyValue($throwable, 'file', Backtrace::file());
|
||||
Reflection::setPropertyValue($throwable, 'line', Backtrace::line());
|
||||
|
||||
|
||||
@ -193,9 +193,7 @@ final class Reflection
|
||||
}
|
||||
|
||||
$arguments[$parameter->getName()] = implode('|', array_map(
|
||||
static function (ReflectionNamedType $type): string {
|
||||
return $type->getName();
|
||||
},
|
||||
static fn (ReflectionNamedType $type): string => $type->getName(),
|
||||
($types instanceof ReflectionNamedType)
|
||||
? [$types] // NOTE: normalize as list of to handle unions
|
||||
: $types->getTypes(),
|
||||
|
||||
@ -33,7 +33,7 @@ final class Str
|
||||
*/
|
||||
public static function startsWith(string $target, string $search): bool
|
||||
{
|
||||
return substr($target, 0, strlen($search)) === $search;
|
||||
return str_starts_with($target, $search);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,4 +48,14 @@ final class Str
|
||||
|
||||
return substr($target, -$length) === $search;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the given string evaluable by an `eval`.
|
||||
*/
|
||||
public static function evaluable(string $code): string
|
||||
{
|
||||
$code = str_replace(' ', '_', $code);
|
||||
|
||||
return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,71 +19,50 @@ final class TestSuite
|
||||
{
|
||||
/**
|
||||
* Holds the current test case.
|
||||
*
|
||||
* @var TestCase|null
|
||||
*/
|
||||
public $test;
|
||||
public ?TestCase $test = null;
|
||||
|
||||
/**
|
||||
* Holds the tests repository.
|
||||
*
|
||||
* @var TestRepository
|
||||
*/
|
||||
public $tests;
|
||||
public TestRepository $tests;
|
||||
|
||||
/**
|
||||
* Holds the before each repository.
|
||||
*
|
||||
* @var BeforeEachRepository
|
||||
*/
|
||||
public $beforeEach;
|
||||
public BeforeEachRepository $beforeEach;
|
||||
|
||||
/**
|
||||
* Holds the before all repository.
|
||||
*
|
||||
* @var BeforeAllRepository
|
||||
*/
|
||||
public $beforeAll;
|
||||
public BeforeAllRepository $beforeAll;
|
||||
|
||||
/**
|
||||
* Holds the after each repository.
|
||||
*
|
||||
* @var AfterEachRepository
|
||||
*/
|
||||
public $afterEach;
|
||||
public AfterEachRepository $afterEach;
|
||||
|
||||
/**
|
||||
* Holds the after all repository.
|
||||
*
|
||||
* @var AfterAllRepository
|
||||
*/
|
||||
public $afterAll;
|
||||
public AfterAllRepository $afterAll;
|
||||
|
||||
/**
|
||||
* Holds the root path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rootPath;
|
||||
|
||||
/**
|
||||
* Holds the test path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $testPath;
|
||||
public string $rootPath;
|
||||
|
||||
/**
|
||||
* Holds an instance of the test suite.
|
||||
*
|
||||
* @var TestSuite
|
||||
*/
|
||||
private static $instance;
|
||||
private static ?TestSuite $instance = null;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the test suite.
|
||||
*/
|
||||
public function __construct(string $rootPath, string $testPath)
|
||||
public function __construct(
|
||||
string $rootPath,
|
||||
public string $testPath)
|
||||
{
|
||||
$this->beforeAll = new BeforeAllRepository();
|
||||
$this->beforeEach = new BeforeEachRepository();
|
||||
@ -92,7 +71,6 @@ final class TestSuite
|
||||
$this->afterAll = new AfterAllRepository();
|
||||
|
||||
$this->rootPath = (string) realpath($rootPath);
|
||||
$this->testPath = $testPath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,14 +2,18 @@
|
||||
|
||||
$file = __DIR__ . DIRECTORY_SEPARATOR . 'after-all-test';
|
||||
|
||||
beforeAll(function () use ($file) {
|
||||
@unlink($file);
|
||||
});
|
||||
|
||||
afterAll(function () use ($file) {
|
||||
unlink($file);
|
||||
@unlink($file);
|
||||
});
|
||||
|
||||
test('deletes file after all', function () use ($file) {
|
||||
file_put_contents($file, 'foo');
|
||||
$this->assertFileExists($file);
|
||||
register_shutdown_function(function () use ($file) {
|
||||
$this->assertFileNotExists($file);
|
||||
register_shutdown_function(function () {
|
||||
// $this->assertFileDoesNotExist($file);
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,7 +12,8 @@ beforeEach(function () {
|
||||
it('throws exception if dataset does not exist', function () {
|
||||
$this->expectException(DatasetDoesNotExist::class);
|
||||
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
|
||||
Datasets::get('first');
|
||||
|
||||
Datasets::resolve('foo', ['first']);
|
||||
});
|
||||
|
||||
it('throws exception if dataset already exist', function () {
|
||||
@ -27,13 +28,13 @@ it('sets closures', function () {
|
||||
yield [1];
|
||||
});
|
||||
|
||||
expect(iterator_to_array(Datasets::get('foo')()))->toBe([[1]]);
|
||||
expect(Datasets::resolve('foo', ['foo']))->toBe(['foo with (1)' => [1]]);
|
||||
});
|
||||
|
||||
it('sets arrays', function () {
|
||||
Datasets::set('bar', [[2]]);
|
||||
|
||||
expect(Datasets::get('bar'))->toBe([[2]]);
|
||||
expect(Datasets::resolve('bar', ['bar']))->toBe(['bar with (2)' => [2]]);
|
||||
});
|
||||
|
||||
it('gets bound to test case object', function () {
|
||||
@ -52,6 +53,7 @@ $datasets = [[1], [2]];
|
||||
|
||||
test('lazy datasets', function ($text) use ($state, $datasets) {
|
||||
$state->text .= $text;
|
||||
|
||||
expect(in_array([$text], $datasets))->toBe(true);
|
||||
})->with($datasets);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Pest\PendingObjects\TestCall;
|
||||
use Pest\PendingCalls\TestCall;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
uses(Gettable::class);
|
||||
|
||||
@ -1,51 +1,45 @@
|
||||
<?php
|
||||
|
||||
global $globalHook;
|
||||
|
||||
// NOTE: this test does not have a $globalHook->calls offset since it is first
|
||||
// in the directory and thus will always run before the others. See also the
|
||||
// BeforeAllTest.php for details.
|
||||
|
||||
uses()->afterAll(function () use ($globalHook) {
|
||||
expect($globalHook)
|
||||
uses()->afterAll(function () {
|
||||
expect($_SERVER['globalHook'])
|
||||
->toHaveProperty('afterAll')
|
||||
->and($globalHook->afterAll)
|
||||
->and($_SERVER['globalHook']->afterAll)
|
||||
->toBe(0)
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->afterAll
|
||||
->toBe(1);
|
||||
|
||||
$globalHook->afterAll = 1;
|
||||
$globalHook->calls->afterAll++;
|
||||
$_SERVER['globalHook']->afterAll = 1;
|
||||
$_SERVER['globalHook']->calls->afterAll++;
|
||||
});
|
||||
|
||||
afterAll(function () use ($globalHook) {
|
||||
expect($globalHook)
|
||||
afterAll(function () {
|
||||
expect($_SERVER['globalHook'])
|
||||
->toHaveProperty('afterAll')
|
||||
->and($globalHook->afterAll)
|
||||
->and($_SERVER['globalHook']->afterAll)
|
||||
->toBe(1)
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->afterAll
|
||||
->toBe(2);
|
||||
|
||||
$globalHook->afterAll = 2;
|
||||
$globalHook->calls->afterAll++;
|
||||
$_SERVER['globalHook']->afterAll = 2;
|
||||
$_SERVER['globalHook']->calls->afterAll++;
|
||||
});
|
||||
|
||||
test('global afterAll execution order', function () use ($globalHook) {
|
||||
expect($globalHook)
|
||||
test('global afterAll execution order', function () {
|
||||
expect($_SERVER['globalHook'])
|
||||
->not()
|
||||
->toHaveProperty('afterAll')
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->afterAll
|
||||
->toBe(0);
|
||||
});
|
||||
|
||||
it('only gets called once per file', function () use ($globalHook) {
|
||||
expect($globalHook)
|
||||
it('only gets called once per file', function () {
|
||||
expect($_SERVER['globalHook'])
|
||||
->not()
|
||||
->toHaveProperty('afterAll')
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->afterAll
|
||||
->toBe(0);
|
||||
});
|
||||
|
||||
@ -2,55 +2,53 @@
|
||||
|
||||
use Pest\Support\Str;
|
||||
|
||||
global $globalHook;
|
||||
|
||||
// HACK: we have to determine our $globalHook->calls baseline. This is because
|
||||
// HACK: we have to determine our $_SERVER['globalHook-]>calls baseline. This is because
|
||||
// two other tests are executed before this one due to filename ordering.
|
||||
$args = $_SERVER['argv'] ?? [];
|
||||
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
|
||||
$offset = $single ? 0 : 2;
|
||||
|
||||
uses()->beforeAll(function () use ($globalHook, $offset) {
|
||||
expect($globalHook)
|
||||
uses()->beforeAll(function () use ($offset) {
|
||||
expect($_SERVER['globalHook'])
|
||||
->toHaveProperty('beforeAll')
|
||||
->and($globalHook->beforeAll)
|
||||
->and($_SERVER['globalHook']->beforeAll)
|
||||
->toBe(0)
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->beforeAll
|
||||
->toBe(1 + $offset);
|
||||
|
||||
$globalHook->beforeAll = 1;
|
||||
$globalHook->calls->beforeAll++;
|
||||
$_SERVER['globalHook']->beforeAll = 1;
|
||||
$_SERVER['globalHook']->calls->beforeAll++;
|
||||
});
|
||||
|
||||
beforeAll(function () use ($globalHook, $offset) {
|
||||
expect($globalHook)
|
||||
beforeAll(function () use ($offset) {
|
||||
expect($_SERVER['globalHook'])
|
||||
->toHaveProperty('beforeAll')
|
||||
->and($globalHook->beforeAll)
|
||||
->and($_SERVER['globalHook']->beforeAll)
|
||||
->toBe(1)
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->beforeAll
|
||||
->toBe(2 + $offset);
|
||||
|
||||
$globalHook->beforeAll = 2;
|
||||
$globalHook->calls->beforeAll++;
|
||||
$_SERVER['globalHook']->beforeAll = 2;
|
||||
$_SERVER['globalHook']->calls->beforeAll++;
|
||||
});
|
||||
|
||||
test('global beforeAll execution order', function () use ($globalHook, $offset) {
|
||||
expect($globalHook)
|
||||
test('global beforeAll execution order', function () use ($offset) {
|
||||
expect($_SERVER['globalHook'])
|
||||
->toHaveProperty('beforeAll')
|
||||
->and($globalHook->beforeAll)
|
||||
->and($_SERVER['globalHook']->beforeAll)
|
||||
->toBe(2)
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->beforeAll
|
||||
->toBe(3 + $offset);
|
||||
});
|
||||
|
||||
it('only gets called once per file', function () use ($globalHook, $offset) {
|
||||
expect($globalHook)
|
||||
it('only gets called once per file', function () use ($offset) {
|
||||
expect($_SERVER['globalHook'])
|
||||
->beforeAll
|
||||
->toBe(2)
|
||||
->and($globalHook->calls)
|
||||
->and($_SERVER['globalHook']->calls)
|
||||
->beforeAll
|
||||
->toBe(3 + $offset);
|
||||
});
|
||||
|
||||
@ -7,7 +7,7 @@ namespace Tests\CustomTestCase;
|
||||
use function PHPUnit\Framework\assertTrue;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CustomTestCase extends TestCase
|
||||
abstract class CustomTestCase extends TestCase
|
||||
{
|
||||
public function assertCustomTrue()
|
||||
{
|
||||
|
||||
@ -7,21 +7,21 @@ uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/S
|
||||
uses()->group('integration')->in('Visual');
|
||||
|
||||
// NOTE: global test value container to be mutated and checked across files, as needed
|
||||
$globalHook = (object) ['calls' => (object) ['beforeAll' => 0, 'afterAll' => 0]];
|
||||
$_SERVER['globalHook'] = (object) ['calls' => (object) ['beforeAll' => 0, 'afterAll' => 0]];
|
||||
|
||||
uses()
|
||||
->beforeEach(function () {
|
||||
$this->baz = 0;
|
||||
})
|
||||
->beforeAll(function () use ($globalHook) {
|
||||
$globalHook->beforeAll = 0;
|
||||
$globalHook->calls->beforeAll++;
|
||||
->beforeAll(function () {
|
||||
$_SERVER['globalHook']->beforeAll = 0;
|
||||
$_SERVER['globalHook']->calls->beforeAll++;
|
||||
})
|
||||
->afterEach(function () {
|
||||
$this->ith = 0;
|
||||
})
|
||||
->afterAll(function () use ($globalHook) {
|
||||
$globalHook->afterAll = 0;
|
||||
$globalHook->calls->afterAll++;
|
||||
->afterAll(function () {
|
||||
$_SERVER['globalHook']->afterAll = 0;
|
||||
$_SERVER['globalHook']->calls->afterAll++;
|
||||
})
|
||||
->in('Hooks');
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
use NunoMaduro\Collision\Adapters\Phpunit\Printer;
|
||||
use Pest\Actions\AddsDefaults;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
|
||||
it('sets defaults', function () {
|
||||
$arguments = AddsDefaults::to(['bar' => 'foo']);
|
||||
|
||||
expect($arguments['printer'])->toBeInstanceOf(Printer::class);
|
||||
expect($arguments['bar'])->toBe('foo');
|
||||
});
|
||||
|
||||
it('does not override options', function () {
|
||||
$defaultResultPrinter = new DefaultResultPrinter();
|
||||
|
||||
expect(AddsDefaults::to(['printer' => $defaultResultPrinter]))->tobe([
|
||||
'printer' => $defaultResultPrinter,
|
||||
]);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user