Merge branch 'next'

This commit is contained in:
Nuno Maduro
2021-11-14 20:00:34 +00:00
102 changed files with 1462 additions and 1565 deletions

View File

@ -8,7 +8,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
php: ['7.3', '7.4', '8.0', '8.1'] php: ['8.0', '8.1']
dependency-version: [prefer-lowest, prefer-stable] dependency-version: [prefer-lowest, prefer-stable]
parallel: ['', '--parallel'] parallel: ['', '--parallel']
exclude: exclude:

View File

@ -3,6 +3,7 @@
$finder = PhpCsFixer\Finder::create() $finder = PhpCsFixer\Finder::create()
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'overrides')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
->append(['.php-cs-fixer.dist.php']); ->append(['.php-cs-fixer.dist.php']);

4
TODO.md Normal file
View 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.

View File

@ -1,11 +1,10 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php declare(strict_types=1); <?php declare(strict_types=1);
use NunoMaduro\Collision\Provider;
use Pest\Actions\ValidatesEnvironment; use Pest\Actions\ValidatesEnvironment;
use Pest\Support\Container; use Pest\Support\Container;
use Pest\Kernel;
use Pest\TestSuite; use Pest\TestSuite;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -25,8 +24,6 @@ use Symfony\Component\Console\Output\OutputInterface;
$autoloadPath = $localPath; $autoloadPath = $localPath;
} }
(new Provider())->register();
// Get $rootPath based on $autoloadPath // Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2); $rootPath = dirname($autoloadPath, 2);
$argv = new ArgvInput(); $argv = new ArgvInput();
@ -40,8 +37,6 @@ use Symfony\Component\Console\Output\OutputInterface;
$container->add(TestSuite::class, $testSuite); $container->add(TestSuite::class, $testSuite);
$container->add(OutputInterface::class, $output); $container->add(OutputInterface::class, $output);
ValidatesEnvironment::in($testSuite);
$args = $_SERVER['argv']; $args = $_SERVER['argv'];
// Let's remove any arguments that PHPUnit does not understand // 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)) { $kernel = Kernel::boot();
$output->writeln("Parallel support requires the Pest Parallel plugin. Run <fg=yellow;options=bold>`composer require --dev pestphp/pest-plugin-parallel`</> first.");
exit(Command::FAILURE);
}
$command = $runInParallel ? \Pest\Parallel\Command::class : \Pest\Console\Command::class; $result = $kernel->handle($args);
exit($container->get($command)->run($args));
$kernel->shutdown();
exit($result);
})(); })();

View File

@ -17,16 +17,21 @@
} }
], ],
"require": { "require": {
"php": "^7.3 || ^8.0", "php": "^8.0",
"nunomaduro/collision": "^5.4.0|^6.0", "nunomaduro/collision": "^5.10.0|^6.0",
"pestphp/pest-plugin": "^1.0.0", "pestphp/pest-plugin": "^1.0.0",
"phpunit/phpunit": "^9.5.5" "phpunit/phpunit": "10.0.x-dev"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Pest\\": "src/" "Pest\\": "src/"
}, },
"exclude-from-classmap": [
"../phpunit/src/Runner/TestSuiteLoader.php",
"vendor/phpunit/phpunit/src/Runner/TestSuiteLoader.php"
],
"files": [ "files": [
"overrides/Runner/TestSuiteLoader.php",
"src/Functions.php", "src/Functions.php",
"src/Pest.php" "src/Pest.php"
] ]
@ -43,8 +48,7 @@
"illuminate/console": "^8.47.0", "illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0", "illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0", "laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master", "pestphp/pest-dev-tools": "dev-master"
"pestphp/pest-plugin-parallel": "^1.0"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,
@ -73,7 +77,7 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.x-dev" "dev-next": "2.x-dev"
}, },
"pest": { "pest": {
"plugins": [ "plugins": [

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -2,18 +2,18 @@
declare(strict_types=1); declare(strict_types=1);
namespace Pest\Actions; namespace Pest\Bootstrappers;
use Pest\Support\Str; use Pest\Support\Str;
use function Pest\testDirectory; use function Pest\testDirectory;
use PHPUnit\Util\FileLoader; use Pest\TestSuite;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
/** /**
* @internal * @internal
*/ */
final class LoadStructure final class BootFiles
{ {
/** /**
* The Pest convention. * The Pest convention.
@ -21,24 +21,23 @@ final class LoadStructure
* @var array<int, string> * @var array<int, string>
*/ */
private const STRUCTURE = [ private const STRUCTURE = [
'Expectations.php', 'Datasets',
'Datasets.php', 'Datasets.php',
'Expectations',
'Expectations.php',
'Helpers',
'Helpers.php', 'Helpers.php',
'Pest.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(); $testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
$load = function ($filename): bool {
return file_exists($filename) && (bool) FileLoader::checkAndLoad($filename);
};
foreach (self::STRUCTURE as $filename) { foreach (self::STRUCTURE as $filename) {
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename); $filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
@ -50,14 +49,21 @@ final class LoadStructure
$directory = new RecursiveDirectoryIterator($filename); $directory = new RecursiveDirectoryIterator($filename);
$iterator = new RecursiveIteratorIterator($directory); $iterator = new RecursiveIteratorIterator($directory);
foreach ($iterator as $file) { foreach ($iterator as $file) {
$filename = $file->__toString(); $this->load($file->__toString());
if (Str::endsWith($filename, '.php') && file_exists($filename)) {
require_once $filename;
}
} }
} else { } 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;
}
}
} }

View File

@ -0,0 +1,37 @@
<?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\EnsureTestsAreLoaded::class,
Subscribers\EnsureConfigurationIsValid::class,
Subscribers\EnsureConfigurationDefaults::class,
];
/**
* Boots the Subscribers.
*/
public function __invoke(): void
{
foreach (self::$subscribers as $subscriber) {
Event\Facade::registerSubscriber(
new $subscriber()
);
}
}
}

View File

@ -14,13 +14,13 @@ trait Expectable
/** /**
* @template TValue * @template TValue
* *
* Creates a new expectation. * Creates a new Expectation.
* *
* @param TValue $value * @param TValue $value
* *
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
public function expect($value): Expectation public function expect(mixed $value): Expectation
{ {
return new Expectation($value); return new Expectation($value);
} }

View File

@ -13,12 +13,14 @@ use Closure;
trait Extendable trait Extendable
{ {
/** /**
* The list of extends.
*
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private static $extends = []; private static array $extends = [];
/** /**
* Register a custom extend. * Register a new extend.
*/ */
public static function extend(string $name, Closure $extend): void public static function extend(string $name, Closure $extend): void
{ {
@ -26,7 +28,7 @@ trait Extendable
} }
/** /**
* Checks if extend is registered. * Checks if given extend name is registered.
*/ */
public static function hasExtend(string $name): bool public static function hasExtend(string $name): bool
{ {
@ -37,10 +39,8 @@ trait Extendable
* Dynamically handle calls to the class. * Dynamically handle calls to the class.
* *
* @param array<int, mixed> $parameters * @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)) { if (!static::hasExtend($method)) {
throw new BadMethodCallException("$method is not a callable method name."); throw new BadMethodCallException("$method is not a callable method name.");

View File

@ -9,21 +9,33 @@ namespace Pest\Concerns\Logging;
*/ */
trait WritesToConsole trait WritesToConsole
{ {
/**
* Writes the given success message to the console.
*/
private function writeSuccess(string $message): void private function writeSuccess(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-green, bold', '✓'); $this->writePestTestOutput($message, 'fg-green, bold', '✓');
} }
/**
* Writes the given error message to the console.
*/
private function writeError(string $message): void private function writeError(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-red, bold', ''); $this->writePestTestOutput($message, 'fg-red, bold', '');
} }
/**
* Writes the given warning message to the console.
*/
private function writeWarning(string $message): void private function writeWarning(string $message): void
{ {
$this->writePestTestOutput($message, 'fg-yellow, bold', '-'); $this->writePestTestOutput($message, 'fg-yellow, bold', '-');
} }
/**
* Writes the give message to the console.
*/
private function writePestTestOutput(string $message, string $color, string $symbol): void private function writePestTestOutput(string $message, string $color, string $symbol): void
{ {
$this->writeWithColor($color, "$symbol ", false); $this->writeWithColor($color, "$symbol ", false);

View File

@ -19,7 +19,7 @@ trait RetrievesValues
* *
* @return TRetrievableValue|null * @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)) { if (is_array($value)) {
return $value[$key] ?? $default; return $value[$key] ?? $default;

View File

@ -8,157 +8,105 @@ use Closure;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\ExecutionOrderDependency;
use Throwable; use Throwable;
/** /**
* To avoid inheritance conflicts, all the fields related
* to Pest only will be prefixed by double underscore.
*
* @internal * @internal
*/ */
trait Testable trait Testable
{ {
/** /**
* The test case description. Contains the first * The Test Case "test" closure.
* argument of global functions like `it` and `test`.
*
* @var string
*/ */
private $__description; private Closure $__test;
/** /**
* Holds the test closure function. * The Test Case "setUp" closure.
*
* @var Closure
*/ */
private $__test; private ?Closure $__beforeEach = null;
/** /**
* Holds a global/shared beforeEach ("set up") closure if one has been * The Test Case "tearDown" closure.
* defined.
*
* @var Closure|null
*/ */
private $beforeEach = null; private ?Closure $__afterEach = null;
/** /**
* Holds a global/shared afterEach ("tear down") closure if one has been * The Test Case "setUpBeforeClass" closure.
* defined.
*
* @var Closure|null
*/ */
private $afterEach = null; private static ?Closure $__beforeAll = null;
/** /**
* Holds a global/shared beforeAll ("set up before") closure if one has been * The test "tearDownAfterClass" closure.
* defined.
*
* @var Closure|null
*/ */
private static $beforeAll = null; private static ?Closure $__afterAll = null;
/** /**
* Holds a global/shared afterAll ("tear down after") closure if one has * Resets the test case static properties.
* been defined.
*
* @var Closure|null
*/ */
private static $afterAll = null; public static function flush(): void
/**
* Creates a new instance of the test case.
*/
public function __construct(Closure $test, string $description, array $data)
{ {
$this->__test = $test; self::$__beforeAll = null;
$this->__description = $description; self::$__afterAll = null;
self::$beforeAll = null;
self::$afterAll = null;
parent::__construct('__test', $data);
} }
/** /**
* 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 public function __addBeforeAll(?Closure $hook): 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
{ {
if (!$hook) { if (!$hook) {
return; return;
} }
self::$beforeAll = (self::$beforeAll instanceof Closure) self::$__beforeAll = (self::$__beforeAll instanceof Closure)
? ChainableClosure::fromStatic(self::$beforeAll, $hook) ? ChainableClosure::fromStatic(self::$__beforeAll, $hook)
: $hook; : $hook;
} }
/** /**
* Add a shared/"global" after all test hook that will execute **before** * Adds a new "tearDownAfterClass" to the Test Case.
* the test defined `afterAll` hook(s).
*/ */
public function addAfterAll(?Closure $hook): void public function __addAfterAll(?Closure $hook): void
{ {
if (!$hook) { if (!$hook) {
return; return;
} }
self::$afterAll = (self::$afterAll instanceof Closure) self::$__afterAll = (self::$__afterAll instanceof Closure)
? ChainableClosure::fromStatic(self::$afterAll, $hook) ? ChainableClosure::fromStatic(self::$__afterAll, $hook)
: $hook; : $hook;
} }
/** /**
* Add a shared/"global" before each test hook that will execute **before** * Adds a new "setUp" to the Test Case.
* the test defined `beforeEach` hook.
*/ */
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** * Adds a new "tearDown" to the Test Case.
* the test defined `afterEach` hook.
*/ */
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) { if (!$hook) {
return; return;
@ -170,22 +118,15 @@ trait Testable
} }
/** /**
* Returns the test case name. Note that, in Pest * Gets the Test Case filename.
* we ignore withDataset argument as the description
* already contains the dataset description.
*/ */
public function getName(bool $withDataSet = true): string public static function __getFilename(): string
{
return $this->__description;
}
public static function __getFileName(): string
{ {
return self::$__filename; 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 public static function setUpBeforeClass(): void
{ {
@ -193,22 +134,22 @@ trait Testable
$beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename); $beforeAll = TestSuite::getInstance()->beforeAll->get(self::$__filename);
if (self::$beforeAll instanceof Closure) { if (self::$__beforeAll instanceof Closure) {
$beforeAll = ChainableClosure::fromStatic(self::$beforeAll, $beforeAll); $beforeAll = ChainableClosure::fromStatic(self::$__beforeAll, $beforeAll);
} }
call_user_func(Closure::bind($beforeAll, null, self::class)); 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 public static function tearDownAfterClass(): void
{ {
$afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename); $afterAll = TestSuite::getInstance()->afterAll->get(self::$__filename);
if (self::$afterAll instanceof Closure) { if (self::$__afterAll instanceof Closure) {
$afterAll = ChainableClosure::fromStatic(self::$afterAll, $afterAll); $afterAll = ChainableClosure::fromStatic(self::$__afterAll, $afterAll);
} }
call_user_func(Closure::bind($afterAll, null, self::class)); 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 protected function setUp(): void
{ {
@ -227,22 +168,22 @@ trait Testable
$beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename); $beforeEach = TestSuite::getInstance()->beforeEach->get(self::$__filename);
if ($this->beforeEach instanceof Closure) { if ($this->__beforeEach instanceof Closure) {
$beforeEach = ChainableClosure::from($this->beforeEach, $beforeEach); $beforeEach = ChainableClosure::from($this->__beforeEach, $beforeEach);
} }
$this->__callClosure($beforeEach, func_get_args()); $this->__callClosure($beforeEach, func_get_args());
} }
/** /**
* Gets executed after the test. * Gets executed after the Test Case.
*/ */
protected function tearDown(): void protected function tearDown(): void
{ {
$afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename); $afterEach = TestSuite::getInstance()->afterEach->get(self::$__filename);
if ($this->afterEach instanceof Closure) { if ($this->__afterEach instanceof Closure) {
$afterEach = ChainableClosure::from($this->afterEach, $afterEach); $afterEach = ChainableClosure::from($this->__afterEach, $afterEach);
} }
$this->__callClosure($afterEach, func_get_args()); $this->__callClosure($afterEach, func_get_args());
@ -253,27 +194,13 @@ trait Testable
} }
/** /**
* Returns the test case as string. * Executes the Test Case current test.
*/
public function toString(): string
{
return \sprintf(
'%s::%s',
self::$__filename,
$this->__description
);
}
/**
* Runs the test.
*
* @return mixed
* *
* @throws Throwable * @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 * @throws Throwable
*/ */
private function resolveTestArguments(array $arguments): array private function __resolveTestArguments(array $arguments): array
{ {
return array_map(function ($data) { return array_map(fn ($data) => $data instanceof Closure ? $this->__callClosure($data, []) : $data, $arguments);
return $data instanceof Closure ? $this->__callClosure($data, []) : $data;
}, $arguments);
} }
/** /**
* @return mixed
*
* @throws Throwable * @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 ExceptionTrace::ensure(fn () => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
return call_user_func_array(Closure::bind($closure, $this, get_class($this)), $arguments);
});
} }
/**
* Gets the Test Case name that should be used by printers.
*/
public function getPrintableTestCaseName(): string public function getPrintableTestCaseName(): string
{ {
return ltrim(self::class, 'P\\'); return ltrim(self::class, 'P\\');

View File

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

View File

@ -11,7 +11,11 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class Help final class Help
{ {
/** @var array<int, string> */ /**
* The Command messages.
*
* @var array<int, string>
*/
private const HELP_MESSAGES = [ private const HELP_MESSAGES = [
'<comment>Pest Options:</comment>', '<comment>Pest Options:</comment>',
' <info>--init</info> Initialise a standard Pest configuration', ' <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)', ' <info>--group=<fg=cyan><name></></info> Only runs tests from the specified group(s)',
]; ];
/** @var OutputInterface */ /**
private $output; * Creates a new Console Command instance.
*/
public function __construct(OutputInterface $output) public function __construct(private OutputInterface $output)
{ {
$this->output = $output; // ..
} }
/**
* Executes the Console Command.
*/
public function __invoke(): void public function __invoke(): void
{ {
foreach (self::HELP_MESSAGES as $message) { foreach (self::HELP_MESSAGES as $message) {

View File

@ -14,7 +14,11 @@ use Symfony\Component\Console\Question\ConfirmationQuestion;
*/ */
final class Thanks final class Thanks
{ {
/** @var array<int, string> */ /**
* The Command messages.
*
* @var array<int, string>
*/
private const FUNDING_MESSAGES = [ private const FUNDING_MESSAGES = [
'', '',
' - Star or contribute to Pest:', ' - Star or contribute to Pest:',
@ -25,16 +29,16 @@ final class Thanks
' <options=bold>https://github.com/sponsors/nunomaduro</>', ' <options=bold>https://github.com/sponsors/nunomaduro</>',
]; ];
/** @var OutputInterface */ /**
private $output; * Creates a new Console Command instance.
*/
public function __construct(OutputInterface $output) public function __construct(private OutputInterface $output)
{ {
$this->output = $output; // ..
} }
/** /**
* Asks the user to support Pest. * Executes the Console Command.
*/ */
public function __invoke(): void public function __invoke(): void
{ {

View File

@ -4,18 +4,12 @@ declare(strict_types=1);
namespace Pest\Contracts; namespace Pest\Contracts;
if (interface_exists(\NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName::class)) { use NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName as BaseHasPrintableTestCaseName;
/**
/**
* @internal * @internal
*/ */
interface HasPrintableTestCaseName extends \NunoMaduro\Collision\Contracts\Adapters\Phpunit\HasPrintableTestCaseName interface HasPrintableTestCaseName extends BaseHasPrintableTestCaseName
{ {
} // ..
} else {
/**
* @internal
*/
interface HasPrintableTestCaseName
{
}
} }

View File

@ -10,7 +10,7 @@ namespace Pest\Contracts\Plugins;
interface AddsOutput 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;
} }

View File

@ -10,11 +10,11 @@ namespace Pest\Contracts\Plugins;
interface HandlesArguments 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;
} }

View File

@ -20,14 +20,21 @@ final class Datasets
* *
* @var array<int|string, Closure|iterable<int|string, mixed>> * @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. * Sets the given.
* *
* @param Closure|iterable<int|string, mixed> $data * @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)) { if (array_key_exists($name, self::$datasets)) {
throw new DatasetAlreadyExist($name); 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> $data
*/ */
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)) { self::$withs[$filename . '>>>' . $description] = $with;
throw new DatasetDoesNotExist($name);
} }
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. * 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 */ /* @phpstan-ignore-next-line */
if (empty($datasets)) { if (empty($dataset)) {
return [$description => []]; return null;
} }
$datasets = self::processDatasets($datasets); $dataset = self::processDatasets($dataset);
$datasetCombinations = self::getDataSetsCombinations($datasets); $datasetCombinations = self::getDataSetsCombinations($dataset);
$dataSetDescriptions = []; $dataSetDescriptions = [];
$dataSetValues = []; $dataSetValues = [];
@ -114,7 +129,11 @@ final class Datasets
$processedDataset = []; $processedDataset = [];
if (is_string($data)) { if (is_string($data)) {
$datasets[$index] = self::get($data); if (!isset(self::$datasets[$data])) {
throw new DatasetDoesNotExist($data);
}
$datasets[$index] = self::$datasets[$data];
} }
if (is_callable($datasets[$index])) { if (is_callable($datasets[$index])) {
@ -161,10 +180,9 @@ final class Datasets
} }
/** /**
* @param int|string $key
* @param array<int, mixed> $data * @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(); $exporter = new Exporter();

View File

@ -11,30 +11,20 @@ namespace Pest;
*/ */
final class Each final class Each
{ {
/** private bool $opposite = false;
* @var Expectation
*/
private $original;
/**
* @var bool
*/
private $opposite = false;
/** /**
* Creates an expectation on each item of the iterable "value". * 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. * Creates a new expectation.
*
* @param mixed $value
*/ */
public function and($value): Expectation public function and(mixed $value): Expectation
{ {
return $this->original->and($value); return $this->original->and($value);
} }

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $filename)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class AfterEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $filename)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class AttributeNotSupportedYet extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $attribute, string $value)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class BeforeEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $filename)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class DatasetAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $name)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $name)
{ {

View File

@ -10,26 +10,22 @@ use NunoMaduro\Collision\Contracts\RenderlessTrace;
use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\ExceptionInterface;
/** /**
* Creates a new instance of dataset is not present for test that has arguments.
*
* @internal * @internal
*/ */
final class DatasetMissing extends BadFunctionCallException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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( parent::__construct(sprintf(
"A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", "A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s",
$name, $name,
count($args), count($arguments),
implode(', ', array_map(static function (string $arg, string $type): string { implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
return sprintf('%s $%s', $type, $arg);
}, array_keys($args), $args)),
$file, $file,
)); ));
} }

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $filename)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class InvalidConsoleArgument extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $message)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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() public function __construct()
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $feature, string $dependency)
{ {

View File

@ -13,7 +13,7 @@ use RuntimeException;
final class ShouldNotHappen extends RuntimeException final class ShouldNotHappen extends RuntimeException
{ {
/** /**
* Creates a new instance of should not happen. * Creates a new Exception instance.
*/ */
public function __construct(Exception $exception) public function __construct(Exception $exception)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $fileName, string $description)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $inUse, string $newOne, string $folder)
{ {

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace 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) public function __construct(string $testCaseClass)
{ {

View File

@ -29,37 +29,26 @@ use Throwable;
*/ */
final class Expectation final class Expectation
{ {
use Extendable { use RetrievesValues, Extendable {
__call as __extendsCall; __call as __extendsCall;
} }
use RetrievesValues;
/**
* The expectation value.
*
* @readonly
*
* @var mixed
*/
public $value;
/** /**
* The exporter instance, if any. * The exporter instance, if any.
* *
* @readonly * @readonly
*
* @var Exporter|null
*/ */
private $exporter; private ?Exporter $exporter = null;
/** /**
* Creates a new expectation. * Creates a new expectation.
* *
* @param TValue $value * @param TValue $value
*/ */
public function __construct($value) public function __construct(
{ public mixed $value
$this->value = $value; ) {
// ..
} }
/** /**
@ -69,7 +58,7 @@ final class Expectation
* *
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
public function and($value): Expectation public function and(mixed $value): Expectation
{ {
return new self($value); return new self($value);
} }
@ -103,9 +92,9 @@ final class Expectation
/** /**
* Send the expectation value to Ray along with all given arguments. * Send the expectation value to Ray along with all given arguments.
* *
* @param mixed $arguments * @param ...mixed $arguments
*/ */
public function ray(...$arguments): self public function ray(mixed ...$arguments): self
{ {
if (function_exists('ray')) { if (function_exists('ray')) {
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
@ -146,9 +135,9 @@ final class Expectation
* *
* @template TSequenceValue * @template TSequenceValue
* *
* @param callable(self, self): void|TSequenceValue ...$callbacks * @param (callable(self, self): void)|TSequenceValue ...$callbacks
*/ */
public function sequence(...$callbacks): Expectation public function sequence(mixed ...$callbacks): Expectation
{ {
if (!is_iterable($this->value)) { if (!is_iterable($this->value)) {
throw new BadMethodCallException('Expectation value is not iterable.'); throw new BadMethodCallException('Expectation value is not iterable.');
@ -187,16 +176,14 @@ final class Expectation
* *
* @template TMatchSubject of array-key * @template TMatchSubject of array-key
* *
* @param callable(): TMatchSubject|TMatchSubject $subject * @param (callable(): TMatchSubject)|TMatchSubject $subject
* @param array<TMatchSubject, (callable(Expectation<TValue>): mixed)|TValue> $expressions * @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 = is_callable($subject)
? $subject ? $subject
: function () use ($subject) { : fn () => $subject;
return $subject;
};
$subject = $subject(); $subject = $subject();
@ -232,12 +219,12 @@ final class Expectation
* @param (callable(): bool)|bool $condition * @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback * @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 = is_callable($condition)
? $condition ? $condition
: static function () use ($condition): bool { : static function () use ($condition): bool {
return (bool) $condition; // @phpstan-ignore-line return $condition; // @phpstan-ignore-line
}; };
return $this->when(!$condition(), $callback); return $this->when(!$condition(), $callback);
@ -249,12 +236,12 @@ final class Expectation
* @param (callable(): bool)|bool $condition * @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback * @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 = is_callable($condition)
? $condition ? $condition
: static function () use ($condition): bool { : static function () use ($condition): bool {
return (bool) $condition; // @phpstan-ignore-line return $condition; // @phpstan-ignore-line
}; };
if ($condition()) { if ($condition()) {
@ -268,10 +255,8 @@ final class Expectation
* Asserts that two variables have the same type and * Asserts that two variables have the same type and
* value. Used on objects, it asserts that two * value. Used on objects, it asserts that two
* variables reference the same object. * variables reference the same object.
*
* @param mixed $expected
*/ */
public function toBe($expected): Expectation public function toBe(mixed $expected): Expectation
{ {
Assert::assertSame($expected, $this->value); Assert::assertSame($expected, $this->value);
@ -330,10 +315,8 @@ final class Expectation
/** /**
* Asserts that the value is greater than $expected. * Asserts that the value is greater than $expected.
*
* @param int|float $expected
*/ */
public function toBeGreaterThan($expected): Expectation public function toBeGreaterThan(int|float $expected): Expectation
{ {
Assert::assertGreaterThan($expected, $this->value); Assert::assertGreaterThan($expected, $this->value);
@ -342,10 +325,8 @@ final class Expectation
/** /**
* Asserts that the value is greater than or equal to $expected. * Asserts that the value is greater than or equal to $expected.
*
* @param int|float $expected
*/ */
public function toBeGreaterThanOrEqual($expected): Expectation public function toBeGreaterThanOrEqual(int|float $expected): Expectation
{ {
Assert::assertGreaterThanOrEqual($expected, $this->value); Assert::assertGreaterThanOrEqual($expected, $this->value);
@ -354,10 +335,8 @@ final class Expectation
/** /**
* Asserts that the value is less than or equal to $expected. * Asserts that the value is less than or equal to $expected.
*
* @param int|float $expected
*/ */
public function toBeLessThan($expected): Expectation public function toBeLessThan(int|float $expected): Expectation
{ {
Assert::assertLessThan($expected, $this->value); Assert::assertLessThan($expected, $this->value);
@ -366,10 +345,8 @@ final class Expectation
/** /**
* Asserts that the value is less than $expected. * Asserts that the value is less than $expected.
*
* @param int|float $expected
*/ */
public function toBeLessThanOrEqual($expected): Expectation public function toBeLessThanOrEqual(int|float $expected): Expectation
{ {
Assert::assertLessThanOrEqual($expected, $this->value); Assert::assertLessThanOrEqual($expected, $this->value);
@ -378,10 +355,8 @@ final class Expectation
/** /**
* Asserts that $needle is an element of the value. * Asserts that $needle is an element of the value.
*
* @param mixed $needles
*/ */
public function toContain(...$needles): Expectation public function toContain(mixed ...$needles): Expectation
{ {
foreach ($needles as $needle) { foreach ($needles as $needle) {
if (is_string($this->value)) { if (is_string($this->value)) {
@ -456,10 +431,8 @@ final class Expectation
/** /**
* Asserts that the value contains the property $name. * Asserts that the value contains the property $name.
*
* @param mixed $value
*/ */
public function toHaveProperty(string $name, $value = null): Expectation public function toHaveProperty(string $name, mixed $value = null): Expectation
{ {
$this->toBeObject(); $this->toBeObject();
@ -489,10 +462,8 @@ final class Expectation
/** /**
* Asserts that two variables have the same value. * Asserts that two variables have the same value.
*
* @param mixed $expected
*/ */
public function toEqual($expected): Expectation public function toEqual(mixed $expected): Expectation
{ {
Assert::assertEquals($expected, $this->value); Assert::assertEquals($expected, $this->value);
@ -507,10 +478,8 @@ final class Expectation
* are sorted before they are compared. When $expected and $this->value * are sorted before they are compared. When $expected and $this->value
* are objects, each object is converted to an array containing all * are objects, each object is converted to an array containing all
* private, protected and public attributes. * private, protected and public attributes.
*
* @param mixed $expected
*/ */
public function toEqualCanonicalizing($expected): Expectation public function toEqualCanonicalizing(mixed $expected): Expectation
{ {
Assert::assertEqualsCanonicalizing($expected, $this->value); Assert::assertEqualsCanonicalizing($expected, $this->value);
@ -520,10 +489,8 @@ final class Expectation
/** /**
* Asserts that the absolute difference between the value and $expected * Asserts that the absolute difference between the value and $expected
* is lower than $delta. * is lower than $delta.
*
* @param mixed $expected
*/ */
public function toEqualWithDelta($expected, float $delta): Expectation public function toEqualWithDelta(mixed $expected, float $delta): Expectation
{ {
Assert::assertEqualsWithDelta($expected, $this->value, $delta); Assert::assertEqualsWithDelta($expected, $this->value, $delta);
@ -555,9 +522,9 @@ final class Expectation
/** /**
* Asserts that the value is an instance of $class. * Asserts that the value is an instance of $class.
* *
* @param string $class * @param class-string $class
*/ */
public function toBeInstanceOf($class): Expectation public function toBeInstanceOf(string $class): Expectation
{ {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
Assert::assertInstanceOf($class, $this->value); Assert::assertInstanceOf($class, $this->value);
@ -708,11 +675,8 @@ final class Expectation
/** /**
* Asserts that the value array has the provided $key. * Asserts that the value array has the provided $key.
*
* @param string|int $key
* @param mixed $value
*/ */
public function toHaveKey($key, $value = null): Expectation public function toHaveKey(string|int $key, mixed $value = null): Expectation
{ {
if (is_object($this->value) && method_exists($this->value, 'toArray')) { if (is_object($this->value) && method_exists($this->value, 'toArray')) {
$array = $this->value->toArray(); $array = $this->value->toArray();
@ -812,9 +776,9 @@ final class Expectation
/** /**
* Asserts that the value array matches the given array subset. * Asserts that the value array matches the given array subset.
* *
* @param array<int|string, mixed> $array * @param iterable<int|string, mixed> $array
*/ */
public function toMatchArray($array): Expectation public function toMatchArray(iterable|object $array): Expectation
{ {
if (is_object($this->value) && method_exists($this->value, 'toArray')) { if (is_object($this->value) && method_exists($this->value, 'toArray')) {
$valueAsArray = $this->value->toArray(); $valueAsArray = $this->value->toArray();
@ -843,9 +807,9 @@ final class Expectation
* Asserts that the value object matches a subset * Asserts that the value object matches a subset
* of the properties of an given object. * of the properties of an given object.
* *
* @param array<string, mixed>|object $object * @param iterable<string, mixed>|object $object
*/ */
public function toMatchObject($object): Expectation public function toMatchObject(iterable|object $object): Expectation
{ {
foreach ((array) $object as $property => $value) { foreach ((array) $object as $property => $value) {
Assert::assertTrue(property_exists($this->value, $property)); Assert::assertTrue(property_exists($this->value, $property));
@ -891,7 +855,7 @@ final class Expectation
* *
* @param (Closure(Throwable): mixed)|string $exception * @param (Closure(Throwable): mixed)|string $exception
*/ */
public function toThrow($exception, string $exceptionMessage = null): Expectation public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation
{ {
$callback = NullClosure::create(); $callback = NullClosure::create();
@ -938,10 +902,8 @@ final class Expectation
/** /**
* Exports the given value. * Exports the given value.
*
* @param mixed $value
*/ */
private function export($value): string private function export(mixed $value): string
{ {
if ($this->exporter === null) { if ($this->exporter === null) {
$this->exporter = new Exporter(); $this->exporter = new Exporter();
@ -971,10 +933,8 @@ final class Expectation
/** /**
* Dynamically calls methods on the class without any arguments * Dynamically calls methods on the class without any arguments
* or creates a new higher order expectation. * 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 (!method_exists($this, $name) && !static::hasExtend($name)) { if (!method_exists($this, $name) && !static::hasExtend($name)) {
return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); return new HigherOrderExpectation($this, $this->retrieve($name, $this->value));

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

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

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

View File

@ -4,16 +4,18 @@ declare(strict_types=1);
namespace Pest\Factories; namespace Pest\Factories;
use Closure;
use ParseError; use ParseError;
use Pest\Concerns; use Pest\Concerns;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Datasets; use Pest\Datasets;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen; 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\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RuntimeException; use RuntimeException;
@ -22,159 +24,84 @@ use RuntimeException;
*/ */
final class TestCaseFactory final class TestCaseFactory
{ {
/** use HigherOrderable;
* Holds the test filename.
*
* @readonly
*
* @var string
*/
public $filename;
/** /**
* Marks this test case as only. * The list of annotations.
* *
* @readonly * @var array<int, class-string>
*
* @var bool
*/ */
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 * @var class-string
* will be created with the given assertions.
*
* @var string|null
*/ */
public $description; public string $class = TestCase::class;
/** /**
* Holds the test closure. * The list of class methods.
* *
* @readonly * @var array<string, TestCaseMethodFactory>
*
* @var Closure
*/ */
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 = []; public array $traits = [
/**
* The FQN of the test case class.
*
* @var string
*/
public $class = TestCase::class;
/**
* An array of FQN of the class traits.
*
* @var array <int, string>
*/
public $traits = [
Concerns\Testable::class, Concerns\Testable::class,
Concerns\Expectable::class, Concerns\Expectable::class,
]; ];
/** /**
* Holds the higher order messages * Creates a new Factory instance.
* for the factory that are proxyble.
*
* @var HigherOrderMessageCollection
*/ */
public $factoryProxies; public function __construct(
public string $filename
) {
$this->bootHigherOrderable();
}
/** public function make(): void
* 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)
{ {
$this->filename = $filename; $methods = array_filter($this->methods, function ($method) {
$this->description = $description; return count($onlyTestCases = $this->methodsUsingOnly()) === 0 || in_array($method, $onlyTestCases, true);
$this->test = $closure ?? function (): void { });
if (Assert::getCount() === 0) {
self::markTestIncomplete(); // @phpstan-ignore-line
}
};
$this->factoryProxies = new HigherOrderMessageCollection(); if (count($this->methods) > 0) {
$this->proxies = new HigherOrderMessageCollection(); $this->evaluate($this->filename, $methods);
$this->chains = new HigherOrderMessageCollection(); }
} }
/** /**
* 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) { if (Environment::name() === Environment::CI) {
throw ShouldNotHappen::fromMessage('Description can not be empty.'); return [];
} }
$chains = $this->chains; return array_filter($this->methods, static fn ($method): bool => $method->only);
$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);
} }
/** /**
* 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) { if ('\\' === DIRECTORY_SEPARATOR) {
// In case Windows, strtolower drive name, like in UsesCall. // In case Windows, strtolower drive name, like in UsesCall.
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', function ($match): string { $filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $filename);
return strtolower($match['drive']);
}, $filename);
} }
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename))); $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
@ -186,9 +113,7 @@ final class TestCaseFactory
// Strip out any %-encoded octets. // Strip out any %-encoded octets.
$relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath); $relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath);
// Remove escaped quote sequences (maintain namespace) // Remove escaped quote sequences (maintain namespace)
$relativePath = str_replace(array_map(function (string $quote): string { $relativePath = str_replace(array_map(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath);
return sprintf('\\%s', $quote);
}, ['\'', '"']), '', $relativePath);
// Limit to A-Z, a-z, 0-9, '_', '-'. // Limit to A-Z, a-z, 0-9, '_', '-'.
$relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath); $relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath);
@ -198,9 +123,9 @@ final class TestCaseFactory
} }
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
$traitsCode = sprintf('use %s;', implode(', ', array_map(function ($trait): string { $traitsCode = sprintf('use %s;', implode(', ', array_map(
return sprintf('\%s', $trait); static fn ($trait): string => sprintf('\%s', $trait), $this->traits))
}, $this->traits))); );
$partsFQN = explode('\\', $classFQN); $partsFQN = explode('\\', $classFQN);
$className = array_pop($partsFQN); $className = array_pop($partsFQN);
@ -212,14 +137,65 @@ final class TestCaseFactory
$classFQN .= $className; $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 (!empty($method->datasets)) {
$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 { try {
eval(" eval("
namespace $namespace; namespace $namespace;
use Pest\Datasets as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode $traitsCode
private static \$__filename = '$filename'; private static \$__filename = '$filename';
$methodsCode
} }
"); ");
} catch (ParseError $caught) { } catch (ParseError $caught) {
@ -230,11 +206,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 if ($method->description === null) {
|| $this->factoryProxies->count('addDependencies') > 0; throw ShouldNotHappen::fromMessage('The test description may not be empty.');
}
if (isset($this->methods[$method->description])) {
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));
} }
} }

View File

@ -0,0 +1,102 @@
<?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();
};
}
$this->bootHigherOrderable();
}
/**
* Makes the Test Case classes.
*/
public function getClosure(TestCase $concrete): Closure
{
$concrete::flush();
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 {
$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;
}
}

View File

@ -4,10 +4,10 @@ declare(strict_types=1);
use Pest\Datasets; use Pest\Datasets;
use Pest\Expectation; use Pest\Expectation;
use Pest\PendingObjects\AfterEachCall; use Pest\PendingCalls\AfterEachCall;
use Pest\PendingObjects\BeforeEachCall; use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingObjects\TestCall; use Pest\PendingCalls\TestCall;
use Pest\PendingObjects\UsesCall; use Pest\PendingCalls\UsesCall;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\Extendable; use Pest\Support\Extendable;
use Pest\Support\HigherOrderTapProxy; use Pest\Support\HigherOrderTapProxy;
@ -18,10 +18,8 @@ use PHPUnit\Framework\TestCase;
* Creates a new expectation. * Creates a new expectation.
* *
* @param mixed $value the Value * @param mixed $value the Value
*
* @return Expectation|Extendable
*/ */
function expect($value = null) function expect($value = null): Expectation|Extendable
{ {
if (func_num_args() === 0) { if (func_num_args() === 0) {
return new Extendable(Expectation::class); return new Extendable(Expectation::class);
@ -60,7 +58,7 @@ if (!function_exists('dataset')) {
* *
* @param Closure|iterable<int|string, mixed> $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); Datasets::set($name, $dataset);
} }

View File

@ -17,39 +17,17 @@ final class HigherOrderExpectation
use Expectable; use Expectable;
use RetrievesValues; use RetrievesValues;
/** private Expectation|Each $expectation;
* @var Expectation
*/
private $original;
/** private bool $opposite = false;
* @var Expectation|Each
*/
private $expectation;
/** private bool $shouldReset = false;
* @var bool
*/
private $opposite = false;
/**
* @var bool
*/
private $shouldReset = false;
/**
* @var string
*/
private $name;
/** /**
* Creates a new higher order expectation. * 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); $this->expectation = $this->expect($value);
} }
@ -72,7 +50,7 @@ final class HigherOrderExpectation
* *
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
public function and($value): Expectation public function and(mixed $value): Expectation
{ {
return $this->expect($value); return $this->expect($value);
} }
@ -118,10 +96,8 @@ final class HigherOrderExpectation
/** /**
* Retrieve the applicable value based on the current reset condition. * 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; return $this->shouldReset ? $this->original->value : $this->expectation->value;
} }

15
src/IgnorableTestCase.php Normal file
View 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
View 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
{
// TODO
}
}

View File

@ -17,7 +17,7 @@ use Pest\TestSuite;
final class PestDatasetCommand extends Command final class PestDatasetCommand extends Command
{ {
/** /**
* The console command name. * The Console Command name.
* *
* @var string * @var string
*/ */
@ -25,7 +25,7 @@ final class PestDatasetCommand extends Command
{--test-directory=tests : The name of the tests directory}'; {--test-directory=tests : The name of the tests directory}';
/** /**
* The console command description. * The Console Command description.
* *
* @var string * @var string
*/ */

View File

@ -14,7 +14,7 @@ use Pest\Laravel\Commands\PestTestCommand;
final class PestServiceProvider extends ServiceProvider final class PestServiceProvider extends ServiceProvider
{ {
/** /**
* Register artisan commands. * Register Artisan Commands.
*/ */
public function register(): void public function register(): void
{ {

View File

@ -16,7 +16,6 @@ use function class_exists;
use DOMDocument; use DOMDocument;
use DOMElement; use DOMElement;
use Exception; use Exception;
use function get_class;
use function method_exists; use function method_exists;
use Pest\Concerns\Testable; use Pest\Concerns\Testable;
use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\AssertionFailedError;
@ -42,65 +41,50 @@ use function trim;
*/ */
final class JUnit extends Printer implements TestListener final class JUnit extends Printer implements TestListener
{ {
/** private DOMDocument $document;
* @var DOMDocument
*/ private DOMElement $root;
private $document;
/** /**
* @var DOMElement * @var array<int, DOMElement>
*/ */
private $root; private array $testSuites = [];
/**
* @var DOMElement[]
*/
private $testSuites = [];
/** /**
* @var int[] * @var int[]
*/ */
private $testSuiteTests = [0]; private array $testSuiteTests = [0];
/** /**
* @var int[] * @var int[]
*/ */
private $testSuiteAssertions = [0]; private array $testSuiteAssertions = [0];
/** /**
* @var int[] * @var int[]
*/ */
private $testSuiteErrors = [0]; private array $testSuiteErrors = [0];
/** /**
* @var int[] * @var int[]
*/ */
private $testSuiteWarnings = [0]; private array $testSuiteWarnings = [0];
/** /**
* @var int[] * @var int[]
*/ */
private $testSuiteFailures = [0]; private array $testSuiteFailures = [0];
/** /**
* @var int[] * @var int[]
*/ */
private $testSuiteSkipped = [0]; private array $testSuiteSkipped = [0];
/** private array $testSuiteTimes = [0];
* @var int[]|float[]
*/
private $testSuiteTimes = [0];
/** private int $testSuiteLevel = 0;
* @var int
*/
private $testSuiteLevel = 0;
/** private ?DOMElement $currentTestCase = null;
* @var DOMElement|null
*/
private $currentTestCase;
public function __construct(string $out) public function __construct(string $out)
{ {
@ -190,7 +174,7 @@ final class JUnit extends Printer implements TestListener
} }
$testSuite->setAttribute('file', $fileName); $testSuite->setAttribute('file', $fileName);
} catch (ReflectionException $e) { } catch (ReflectionException) {
// @ignoreException // @ignoreException
} }
} }
@ -313,7 +297,7 @@ final class JUnit extends Printer implements TestListener
$testCase->setAttribute('class', $test->getPrintableTestCaseName()); $testCase->setAttribute('class', $test->getPrintableTestCaseName());
$testCase->setAttribute('classname', str_replace('\\', '.', $test->getPrintableTestCaseName())); $testCase->setAttribute('classname', str_replace('\\', '.', $test->getPrintableTestCaseName()));
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
$testCase->setAttribute('file', $test->__getFileName()); $testCase->setAttribute('file', $test->__getFilename());
} }
$this->currentTestCase = $testCase; $this->currentTestCase = $testCase;
@ -409,7 +393,7 @@ final class JUnit extends Printer implements TestListener
if ($t instanceof ExceptionWrapper) { if ($t instanceof ExceptionWrapper) {
$fault->setAttribute('type', $t->getClassName()); $fault->setAttribute('type', $t->getClassName());
} else { } else {
$fault->setAttribute('type', get_class($t)); $fault->setAttribute('type', $t::class);
} }
$this->currentTestCase->appendChild($fault); $this->currentTestCase->appendChild($fault);

View File

@ -16,9 +16,9 @@ use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning; use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\DefaultResultPrinter; use PHPUnit\TextUI\DefaultResultPrinter;
use PHPUnit\TextUI\XmlConfiguration\Logging\TeamCity as BaseTeamCity;
use function round; use function round;
use function str_replace; use function str_replace;
use function strlen;
use Throwable; use Throwable;
final class TeamCity extends DefaultResultPrinter final class TeamCity extends DefaultResultPrinter
@ -34,22 +34,19 @@ final class TeamCity extends DefaultResultPrinter
private const TEST_STARTED = 'testStarted'; private const TEST_STARTED = 'testStarted';
private const TEST_FINISHED = 'testFinished'; private const TEST_FINISHED = 'testFinished';
/** @var int */ private ?int $flowId = null;
private $flowId;
/** @var bool */ private bool $isSummaryTestCountPrinted = false;
private $isSummaryTestCountPrinted = false;
/** @var \PHPUnit\Util\Log\TeamCity */ private BaseTeamCity $phpunitTeamCity;
private $phpunitTeamCity;
/** /**
* @param resource|string|null $out * Creates a new printer instance.
*/ */
public function __construct($out, bool $verbose, string $colors) public function __construct(string|null $out, bool $verbose, string $colors)
{ {
parent::__construct($out, $verbose, $colors); parent::__construct($out, $verbose, $colors);
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity($out, $verbose, $colors); $this->phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors);
$this->logo(); $this->logo();
} }
@ -74,9 +71,7 @@ final class TeamCity extends DefaultResultPrinter
'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'], 'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'],
]; ];
$filteredResults = array_filter($results, function ($item): bool { $filteredResults = array_filter($results, fn ($item): bool => $item['count'] > 0);
return $item['count'] > 0;
});
foreach ($filteredResults as $key => $info) { foreach ($filteredResults as $key => $info) {
$this->writeWithColor($info['color'], $info['count'] . " $key", false); $this->writeWithColor($info['color'], $info['count'] . " $key", false);
@ -203,7 +198,7 @@ final class TeamCity extends DefaultResultPrinter
*/ */
private static function isPestTestSuite(TestSuite $suite): bool private static function isPestTestSuite(TestSuite $suite): bool
{ {
return strncmp($suite->getName(), 'P\\', strlen('P\\')) === 0; return str_starts_with($suite->getName(), 'P\\');
} }
/** /**

View File

@ -14,17 +14,12 @@ use SebastianBergmann\Exporter\Exporter;
*/ */
final class OppositeExpectation final class OppositeExpectation
{ {
/**
* @var Expectation
*/
private $original;
/** /**
* Creates a new opposite expectation. * 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) { foreach ($keys as $key) {
try { try {
$this->original->toHaveKey($key); $this->original->toHaveKey($key);
} catch (ExpectationFailedException $e) { } catch (ExpectationFailedException) {
continue; continue;
} }
@ -57,7 +52,7 @@ final class OppositeExpectation
try { try {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
$this->original->{$name}(...$arguments); $this->original->{$name}(...$arguments);
} catch (ExpectationFailedException $e) { } catch (ExpectationFailedException) {
return $this->original; return $this->original;
} }
@ -73,7 +68,7 @@ final class OppositeExpectation
try { try {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
$this->original->{$name}; $this->original->{$name};
} catch (ExpectationFailedException $e) { } catch (ExpectationFailedException) {
return $this->original; return $this->original;
} }
@ -90,10 +85,8 @@ final class OppositeExpectation
{ {
$exporter = new Exporter(); $exporter = new Exporter();
$toString = function ($argument) use ($exporter): string { $toString = fn ($argument): string => $exporter->shortenedExport($argument);
return $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))));
} }
} }

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Pest\PendingObjects; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
@ -17,47 +17,30 @@ use Pest\TestSuite;
final class AfterEachCall final class AfterEachCall
{ {
/** /**
* Holds the test suite. * The "afterEach" closure.
*
* @var TestSuite
*/ */
private $testSuite; private Closure $closure;
/** /**
* Holds the filename. * The calls that should be proxied.
*
* @var string
*/ */
private $filename; private HigherOrderMessageCollection $proxies;
/** /**
* Holds the before each closure. * Creates a new Pending Call.
*
* @var Closure
*/ */
private $closure; public function __construct(
private TestSuite $testSuite,
/** private string $filename,
* Holds calls that should be proxied. Closure $closure = null
* ) {
* @var HigherOrderMessageCollection
*/
private $proxies;
/**
* Creates a new instance of before each call.
*/
public function __construct(TestSuite $testSuite, string $filename, Closure $closure = null)
{
$this->testSuite = $testSuite;
$this->filename = $filename;
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
$this->proxies = new HigherOrderMessageCollection(); $this->proxies = new HigherOrderMessageCollection();
} }
/** /**
* Dispatch the creation of each call. * Creates the Call.
*/ */
public function __destruct() public function __destruct()
{ {

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Pest\PendingObjects; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
@ -16,48 +16,31 @@ use Pest\TestSuite;
*/ */
final class BeforeEachCall final class BeforeEachCall
{ {
/**
* Holds the test suite.
*
* @var TestSuite
*/
private $testSuite;
/**
* Holds the filename.
*
* @var string
*/
private $filename;
/** /**
* Holds the before each closure. * Holds the before each closure.
*
* @var Closure
*/ */
private $closure; private \Closure $closure;
/** /**
* Holds calls that should be proxied. * The calls that should be proxied.
*
* @var HigherOrderMessageCollection
*/ */
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) public function __construct(
{ private TestSuite $testSuite,
$this->testSuite = $testSuite; private string $filename,
$this->filename = $filename; Closure $closure = null
) {
$this->closure = $closure instanceof Closure ? $closure : NullClosure::create(); $this->closure = $closure instanceof Closure ? $closure : NullClosure::create();
$this->proxies = new HigherOrderMessageCollection(); $this->proxies = new HigherOrderMessageCollection();
} }
/** /**
* Dispatch the creation of each call. * Creates the Call.
*/ */
public function __destruct() public function __destruct()
{ {

View File

@ -2,10 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace Pest\PendingObjects; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\HigherOrderCallables; use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
@ -20,39 +20,25 @@ use SebastianBergmann\Exporter\Exporter;
final class TestCall final class TestCall
{ {
/** /**
* Holds the test suite. * The Test Case Factory.
*
* @readonly
*
* @var TestSuite
*/ */
private $testSuite; private TestCaseMethodFactory $testCaseMethod;
/**
* Holds the test case factory.
*
* @readonly
*
* @var TestCaseFactory
*/
private $testCaseFactory;
/** /**
* If test call is descriptionLess. * 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) public function __construct(
{ private TestSuite $testSuite,
$this->testCaseFactory = new TestCaseFactory($filename, $description, $closure); string $filename,
$this->testSuite = $testSuite; string $description = null,
Closure $closure = null
) {
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
$this->descriptionLess = $description === null; $this->descriptionLess = $description === null;
} }
@ -62,7 +48,7 @@ final class TestCall
public function throws(string $exception, string $exceptionMessage = null): TestCall public function throws(string $exception, string $exceptionMessage = null): TestCall
{ {
if (class_exists($exception)) { if (class_exists($exception)) {
$this->testCaseFactory $this->testCaseMethod
->proxies ->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]); ->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]);
} else { } else {
@ -70,7 +56,7 @@ final class TestCall
} }
if (is_string($exceptionMessage)) { if (is_string($exceptionMessage)) {
$this->testCaseFactory $this->testCaseMethod
->proxies ->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]); ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]);
} }
@ -83,12 +69,12 @@ final class TestCall
* *
* @param (callable(): bool)|bool $condition * @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 = is_callable($condition)
? $condition ? $condition
: static function () use ($condition): bool { : static function () use ($condition): bool {
return (bool) $condition; // @phpstan-ignore-line return $condition; // @phpstan-ignore-line
}; };
if ($condition()) { if ($condition()) {
@ -104,10 +90,10 @@ final class TestCall
* *
* @param array<\Closure|iterable<int|string, mixed>|string> $data * @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) { foreach ($data as $dataset) {
$this->testCaseFactory->datasets[] = $dataset; $this->testCaseMethod->datasets[] = $dataset;
} }
return $this; return $this;
@ -116,11 +102,11 @@ final class TestCall
/** /**
* Sets the test depends. * Sets the test depends.
*/ */
public function depends(string ...$tests): TestCall public function depends(string ...$depends): TestCall
{ {
$this->testCaseFactory foreach ($depends as $depend) {
->factoryProxies $this->testCaseMethod->depends[] = $depend;
->add(Backtrace::file(), Backtrace::line(), 'addDependencies', [$tests]); }
return $this; return $this;
} }
@ -130,7 +116,7 @@ final class TestCall
*/ */
public function only(): TestCall public function only(): TestCall
{ {
$this->testCaseFactory->only = true; $this->testCaseMethod->only = true;
return $this; return $this;
} }
@ -140,19 +126,17 @@ final class TestCall
*/ */
public function group(string ...$groups): TestCall public function group(string ...$groups): TestCall
{ {
$this->testCaseFactory foreach ($groups as $group) {
->factoryProxies $this->testCaseMethod->groups[] = $group;
->add(Backtrace::file(), Backtrace::line(), 'addGroups', [$groups]); }
return $this; return $this;
} }
/** /**
* Skips the current test. * 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) $condition = is_string($conditionOrMessage)
? NullClosure::create() ? NullClosure::create()
@ -160,9 +144,7 @@ final class TestCall
$condition = is_callable($condition) $condition = is_callable($condition)
? $condition ? $condition
: function () use ($condition) { /* @phpstan-ignore-line */ : fn () => $condition;
return $condition;
};
$message = is_string($conditionOrMessage) $message = is_string($conditionOrMessage)
? $conditionOrMessage ? $conditionOrMessage
@ -171,7 +153,7 @@ final class TestCall
/** @var callable(): bool $condition */ /** @var callable(): bool $condition */
$condition = $condition->bindTo(null); $condition = $condition->bindTo(null);
$this->testCaseFactory $this->testCaseMethod
->chains ->chains
->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]); ->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
@ -203,16 +185,16 @@ final class TestCall
*/ */
private function addChain(string $name, array $arguments = null): self private function addChain(string $name, array $arguments = null): self
{ {
$this->testCaseFactory $this->testCaseMethod
->chains ->chains
->add(Backtrace::file(), Backtrace::line(), $name, $arguments); ->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
if ($this->descriptionLess) { if ($this->descriptionLess) {
$exporter = new Exporter(); $exporter = new Exporter();
if ($this->testCaseFactory->description !== null) { if ($this->testCaseMethod->description !== null) {
$this->testCaseFactory->description .= ' → '; $this->testCaseMethod->description .= ' → ';
} }
$this->testCaseFactory->description .= $arguments === null $this->testCaseMethod->description .= $arguments === null
? $name ? $name
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments)); : sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
} }
@ -221,11 +203,10 @@ final class TestCall
} }
/** /**
* Adds the current test case factory * Creates the Call.
* to the tests repository.
*/ */
public function __destruct() public function __destruct()
{ {
$this->testSuite->tests->set($this->testCaseFactory); $this->testSuite->tests->set($this->testCaseMethod);
} }
} }

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace Pest\PendingObjects; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\TestSuite; use Pest\TestSuite;
@ -24,45 +24,31 @@ final class UsesCall
* *
* @var array<int, Closure> * @var array<int, Closure>
*/ */
private $hooks = []; private array $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;
/** /**
* Holds the targets of the uses. * Holds the targets of the uses.
* *
* @var array<int, string> * @var array<int, string>
*/ */
private $targets; private array $targets;
/** /**
* Holds the groups of the uses. * Holds the groups of the uses.
* *
* @var array<int, string> * @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 * @param array<int, string> $classAndTraits
*/ */
public function __construct(string $filename, array $classAndTraits) public function __construct(
{ private string $filename,
$this->classAndTraits = $classAndTraits; private array $classAndTraits
$this->filename = $filename; ) {
$this->targets = [$filename]; $this->targets = [$filename];
} }
@ -76,14 +62,12 @@ final class UsesCall
$startChar = DIRECTORY_SEPARATOR; $startChar = DIRECTORY_SEPARATOR;
if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) { if ('\\' === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0) {
$path = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', function ($match): string { $path = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', fn ($match): string => strtolower($match['drive']), $path);
return strtolower($match['drive']);
}, $path);
$startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__)); $startChar = strtolower((string) preg_replace('~^([a-z]+:\\\).*$~i', '$1', __DIR__));
} }
return 0 === strpos($path, $startChar) return str_starts_with($path, $startChar)
? $path ? $path
: implode(DIRECTORY_SEPARATOR, [ : implode(DIRECTORY_SEPARATOR, [
dirname($this->filename), dirname($this->filename),
@ -151,7 +135,7 @@ final class UsesCall
} }
/** /**
* Dispatch the creation of uses. * Creates the Call.
*/ */
public function __destruct() public function __destruct()
{ {

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string function version(): string
{ {
return '1.20.0'; return '2.x-dev';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string

View File

@ -14,7 +14,7 @@ final class Plugin
* *
* @internal * @internal
*/ */
public static $callables = []; public static array $callables = [];
/** /**
* Lazy loads an `uses` call on the context of plugins. * Lazy loads an `uses` call on the context of plugins.

View File

@ -0,0 +1,29 @@
<?php
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\AddsOutpu $plugin */
foreach ($plugins as $plugin) {
$exitCode = $plugin->addOutput($exitCode);
}
return $exitCode;
}
}

View File

@ -0,0 +1,33 @@
<?php
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;
}
}

View File

@ -28,32 +28,31 @@ final class Coverage implements AddsOutput, HandlesArguments
private const MIN_OPTION = 'min'; private const MIN_OPTION = 'min';
/** /**
* Whether should show the coverage or not. * Whether it should show the coverage or not.
*
* @var bool
*/ */
public $coverage = false; public bool $coverage = false;
/** /**
* The minimum coverage. * 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(private OutputInterface $output)
public function __construct(OutputInterface $output)
{ {
$this->output = $output; // ..
} }
/**
* @param array<int, string> $originals
*
* @return array<int, string>
*/
public function handleArguments(array $originals): array 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) { foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
if ($original === sprintf('--%s', $option) || Str::startsWith($original, sprintf('--%s=', $option))) { if ($original === sprintf('--%s', $option) || Str::startsWith($original, sprintf('--%s=', $option))) {
return true; return true;
@ -61,7 +60,7 @@ final class Coverage implements AddsOutput, HandlesArguments
} }
return false; return false;
}))); }))];
$originals = array_flip($originals); $originals = array_flip($originals);
foreach ($arguments as $argument) { foreach ($arguments as $argument) {

View File

@ -21,17 +21,10 @@ final class Environment implements HandlesArguments
*/ */
public const LOCAL = 'local'; public const LOCAL = 'local';
/**
* @var \Pest\Plugins\Environment|null
*/
private static $instance;
/** /**
* The current environment. * The current environment.
*
* @var string|null
*/ */
private static $name; private static ?string $name = null;
/** /**
* Allows to handle custom command line arguments. * Allows to handle custom command line arguments.

View File

@ -28,23 +28,14 @@ final class Init implements HandlesArguments
'ExampleTest.php' => 'tests/ExampleTest.php', 'ExampleTest.php' => 'tests/ExampleTest.php',
]; ];
/**
* @var OutputInterface
*/
private $output;
/**
* @var TestSuite
*/
private $testSuite;
/** /**
* Creates a new Plugin instance. * Creates a new Plugin instance.
*/ */
public function __construct(TestSuite $testSuite, OutputInterface $output) public function __construct(
{ private TestSuite $testSuite,
$this->testSuite = $testSuite; private OutputInterface $output
$this->output = $output; ) {
// ..
} }
public function handleArguments(array $arguments): array public function handleArguments(array $arguments): array

View File

@ -13,17 +13,13 @@ use Symfony\Component\Console\Output\OutputInterface;
*/ */
final class Version implements HandlesArguments final class Version implements HandlesArguments
{ {
/**
* @var OutputInterface
*/
private $output;
/** /**
* Creates a new instance of the plugin. * Creates a new instance of the plugin.
*/ */
public function __construct(OutputInterface $output) public function __construct(
{ private OutputInterface $output
$this->output = $output; ) {
// ..
} }
public function handleArguments(array $arguments): array public function handleArguments(array $arguments): array

View File

@ -17,7 +17,7 @@ final class AfterAllRepository
/** /**
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private $state = []; private array $state = [];
/** /**
* Runs the given closure for each after all. * Runs the given closure for each after all.

View File

@ -18,7 +18,7 @@ final class AfterEachRepository
/** /**
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private $state = []; private array $state = [];
/** /**
* Sets a after each closure. * Sets a after each closure.
@ -41,6 +41,7 @@ final class AfterEachRepository
return ChainableClosure::from(function (): void { return ChainableClosure::from(function (): void {
if (class_exists(Mockery::class)) { if (class_exists(Mockery::class)) {
/* @phpstan-ignore-next-line */
if ($container = Mockery::getContainer()) { if ($container = Mockery::getContainer()) {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
$this->addToAssertionCount($container->mockery_getExpectationCount()); $this->addToAssertionCount($container->mockery_getExpectationCount());

View File

@ -17,7 +17,7 @@ final class BeforeAllRepository
/** /**
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private $state = []; private array $state = [];
/** /**
* Runs one before all closure, and unsets it from the repository. * Runs one before all closure, and unsets it from the repository.

View File

@ -16,7 +16,7 @@ final class BeforeEachRepository
/** /**
* @var array<string, Closure> * @var array<string, Closure>
*/ */
private $state = []; private array $state = [];
/** /**
* Sets a before each closure. * Sets a before each closure.

View File

@ -5,16 +5,11 @@ declare(strict_types=1);
namespace Pest\Repositories; namespace Pest\Repositories;
use Closure; use Closure;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound; use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseFactory;
use Pest\Plugins\Environment; use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Reflection;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -22,27 +17,22 @@ use PHPUnit\Framework\TestCase;
*/ */
final class TestRepository final class TestRepository
{ {
/**
* @var non-empty-string
*/
private const SEPARATOR = '>>>';
/** /**
* @var array<string, TestCaseFactory> * @var array<string, TestCaseFactory>
*/ */
private $state = []; private array $testCases = [];
/** /**
* @var array<string, array<int, array<int, string|Closure>>> * @var array<string, array<int, array<int, string|Closure>>>
*/ */
private $uses = []; private array $uses = [];
/** /**
* Counts the number of test cases. * Counts the number of test cases.
*/ */
public function count(): int public function count(): int
{ {
return count($this->state); return count($this->testCases);
} }
/** /**
@ -52,81 +42,13 @@ final class TestRepository
*/ */
public function getFilenames(): array 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 { if (count($testCases) === 0) {
return $factory->filename; $testCases = $this->testCases;
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
} }
/** return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $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;
});
} }
/** /**
@ -148,8 +70,8 @@ final class TestRepository
foreach ($paths as $path) { foreach ($paths as $path) {
if (array_key_exists($path, $this->uses)) { if (array_key_exists($path, $this->uses)) {
$this->uses[$path] = [ $this->uses[$path] = [
array_merge($this->uses[$path][0], $classOrTraits), [...$this->uses[$path][0], ...$classOrTraits],
array_merge($this->uses[$path][1], $groups), [...$this->uses[$path][1], ...$groups],
$this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices $this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices
]; ];
} else { } else {
@ -158,27 +80,73 @@ final class TestRepository
} }
} }
/** public function get($filename): TestCaseFactory
* Sets a test case by the given filename and description.
*/
public function set(TestCaseFactory $test): void
{ {
if ($test->description === null) { return $this->testCases[$filename];
throw ShouldNotHappen::fromMessage('Trying to create a test without description.');
} }
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 (!isset($this->testCases[$method->filename])) {
$this->testCases[$method->filename] = new TestCaseFactory($method->filename);
} }
if (!$test->receivesArguments()) { $this->testCases[$method->filename]->addMethod($method);
$arguments = Reflection::getFunctionArguments($test->test); }
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 (isset($this->testCases[$filename])) {
$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();
} }
} }

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

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

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Pest\Subscribers;
use PHPUnit\Event\TestSuite\Loaded;
use PHPUnit\Event\TestSuite\LoadedSubscriber;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\WarningTestCase;
/**
* @internal
*/
final class EnsureTestsAreLoaded implements LoadedSubscriber
{
/**
* The current test suite, if any.
*/
private static ?TestSuite $testSuite = null;
/**
* Runs the subscriber.
*/
public function notify(Loaded $event): void
{
/*
$this->removeWarnings(self::$testSuite);
$testSuites = [];
$testSuite = \Pest\TestSuite::getInstance();
$testSuite->tests->build($testSuite, function (TestCase $testCase) use (&$testSuites): void {
$testCaseClass = $testCase::class;
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->groups());
}
self::$testSuite->addTestSuite($testTestSuite);
}
*/
}
/**
* Sets the current test suite.
*/
public static function setTestSuite(TestSuite $testSuite): void
{
self::$testSuite = $testSuite;
}
/**
* Removes the test case that have "empty" warnings.
*/
private function removeWarnings(TestSuite $testSuite): void
{
$tests = $testSuite->tests();
foreach ($tests as $key => $test) {
if ($test instanceof TestSuite) {
$this->removeWarnings($test);
}
if ($test instanceof WarningTestCase) {
unset($tests[$key]);
}
}
$testSuite->setTests(array_values($tests));
}
}

View File

@ -5,19 +5,14 @@ declare(strict_types=1);
namespace Pest\Support; 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 * @internal
*/ */
final class Arr final class Arr
{ {
/** /**
* @param array<mixed> $array * Checks if the given array has the given key.
* @param string|int $key
*/ */
public static function has(array $array, $key): bool public static function has(array $array, string|int $key): bool
{ {
$key = (string) $key; $key = (string) $key;
@ -37,13 +32,9 @@ final class Arr
} }
/** /**
* @param array<mixed> $array * Gets the given key value.
* @param string|int $key
* @param null $default
*
* @return array|mixed|null
*/ */
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; $key = (string) $key;
@ -51,7 +42,7 @@ final class Arr
return $array[$key]; return $array[$key];
} }
if (strpos($key, '.') === false) { if (!str_contains($key, '.')) {
return $array[$key] ?? $default; return $array[$key] ?? $default;
} }

View File

@ -26,7 +26,7 @@ final class Backtrace
$current = null; $current = null;
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) { 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; break;
} }

View File

@ -18,9 +18,9 @@ final class ChainableClosure
{ {
return function () use ($closure, $next): void { return function () use ($closure, $next): void {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
call_user_func_array(Closure::bind($closure, $this, get_class($this)), func_get_args()); call_user_func_array(Closure::bind($closure, $this, $this::class), func_get_args());
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
call_user_func_array(Closure::bind($next, $this, get_class($this)), func_get_args()); call_user_func_array(Closure::bind($next, $this, $this::class), func_get_args());
}; };
} }

View File

@ -13,15 +13,12 @@ use ReflectionParameter;
*/ */
final class Container final class Container
{ {
/** private static ?Container $instance = null;
* @var self
*/
private static $instance;
/** /**
* @var array<string, mixed> * @var array<string, mixed>
*/ */
private $instances = []; private array $instances = [];
/** /**
* Gets a new or already existing container. * Gets a new or already existing container.

View File

@ -162,7 +162,7 @@ final class Coverage
$lastKey = count($array) - 1; $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]); [$from] = explode('..', $array[$lastKey]);
$array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from); $array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);

View File

@ -8,19 +8,13 @@ use Closure;
final class Extendable final class Extendable
{ {
/**
* The extendable class.
*
* @var string
*/
private $extendableClass;
/** /**
* Creates a new extendable instance. * Creates a new extendable instance.
*/ */
public function __construct(string $extendableClass) public function __construct(
{ private string $extendableClass
$this->extendableClass = $extendableClass; ) {
// ..
} }
/** /**

View File

@ -6,8 +6,6 @@ namespace Pest\Support;
use Closure; use Closure;
use Pest\Expectation; use Pest\Expectation;
use Pest\PendingObjects\TestCall;
use PHPUnit\Framework\TestCase;
/** /**
* @internal * @internal
@ -15,13 +13,11 @@ use PHPUnit\Framework\TestCase;
final class HigherOrderCallables final class HigherOrderCallables
{ {
/** /**
* @var object * Creates a new Higher Order Callables instances.
*/ */
private $target; public function __construct(private object $target)
public function __construct(object $target)
{ {
$this->target = $target; // ..
} }
/** /**
@ -33,7 +29,7 @@ final class HigherOrderCallables
* *
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
public function expect($value) public function expect(mixed $value): Expectation
{ {
return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value); return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value);
} }
@ -47,17 +43,15 @@ final class HigherOrderCallables
* *
* @return Expectation<TValue> * @return Expectation<TValue>
*/ */
public function and($value) public function and(mixed $value)
{ {
return $this->expect($value); return $this->expect($value);
} }
/** /**
* Tap into the test case to perform an action and return the test case. * 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); Reflection::bindCallableWithData($callable);

View File

@ -15,68 +15,31 @@ final class HigherOrderMessage
{ {
public const UNDEFINED_METHOD = 'Method %s does not exist'; 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. * An optional condition that will determine if the message will be executed.
* *
* @var callable(): bool|null * @var (callable(): bool)|null
*/ */
public $condition = null; public $condition;
/** /**
* Creates a new higher order message. * Creates a new higher order message.
* *
* @param array<int, mixed>|null $arguments * @param array<int, mixed>|null $arguments
*/ */
public function __construct(string $filename, int $line, string $methodName, $arguments) public function __construct(
{ public string $filename,
$this->filename = $filename; public int $line,
$this->line = $line; public string $name,
$this->name = $methodName; public ?array $arguments
$this->arguments = $arguments; ) {
// ..
} }
/** /**
* Re-throws the given `$throwable` with the good line and filename. * 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 */ /* @phpstan-ignore-next-line */
if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) { if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) {
@ -122,10 +85,8 @@ final class HigherOrderMessage
/** /**
* Determines whether or not there exists a higher order callable with the message name. * 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); return in_array($this->name, get_class_methods(HigherOrderCallables::class), true);
} }
@ -133,7 +94,7 @@ final class HigherOrderMessage
private static function getUndefinedMethodMessage(object $target, string $methodName): string private static function getUndefinedMethodMessage(object $target, string $methodName): string
{ {
if (\PHP_MAJOR_VERSION >= 8) { 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); return sprintf(self::UNDEFINED_METHOD, $methodName);

View File

@ -12,7 +12,7 @@ final class HigherOrderMessageCollection
/** /**
* @var array<int, HigherOrderMessage> * @var array<int, HigherOrderMessage>
*/ */
private $messages = []; private array $messages = [];
/** /**
* Adds a new higher order message to the collection. * Adds a new higher order message to the collection.
@ -63,9 +63,7 @@ final class HigherOrderMessageCollection
{ {
return array_reduce( return array_reduce(
$this->messages, $this->messages,
static function (int $total, HigherOrderMessage $message) use ($name): int { static fn (int $total, HigherOrderMessage $message): int => $total + (int) ($name === $message->name),
return $total + (int) ($name === $message->name);
},
0, 0,
); );
} }

View File

@ -15,19 +15,13 @@ final class HigherOrderTapProxy
{ {
private const UNDEFINED_PROPERTY = 'Undefined property: P\\'; private const UNDEFINED_PROPERTY = 'Undefined property: P\\';
/**
* The target being tapped.
*
* @var TestCase
*/
public $target;
/** /**
* Create a new tap proxy instance. * Create a new tap proxy instance.
*/ */
public function __construct(TestCase $target) public function __construct(
{ public TestCase $target
$this->target = $target; ) {
// ..
} }
/** /**

View File

@ -193,9 +193,7 @@ final class Reflection
} }
$arguments[$parameter->getName()] = implode('|', array_map( $arguments[$parameter->getName()] = implode('|', array_map(
static function (ReflectionNamedType $type): string { static fn (ReflectionNamedType $type): string => $type->getName(),
return $type->getName();
},
($types instanceof ReflectionNamedType) ($types instanceof ReflectionNamedType)
? [$types] // NOTE: normalize as list of to handle unions ? [$types] // NOTE: normalize as list of to handle unions
: $types->getTypes(), : $types->getTypes(),

View File

@ -33,7 +33,7 @@ final class Str
*/ */
public static function startsWith(string $target, string $search): bool 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; 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);
}
} }

View File

@ -19,71 +19,50 @@ final class TestSuite
{ {
/** /**
* Holds the current test case. * Holds the current test case.
*
* @var TestCase|null
*/ */
public $test; public ?TestCase $test = null;
/** /**
* Holds the tests repository. * Holds the tests repository.
*
* @var TestRepository
*/ */
public $tests; public TestRepository $tests;
/** /**
* Holds the before each repository. * Holds the before each repository.
*
* @var BeforeEachRepository
*/ */
public $beforeEach; public BeforeEachRepository $beforeEach;
/** /**
* Holds the before all repository. * Holds the before all repository.
*
* @var BeforeAllRepository
*/ */
public $beforeAll; public BeforeAllRepository $beforeAll;
/** /**
* Holds the after each repository. * Holds the after each repository.
*
* @var AfterEachRepository
*/ */
public $afterEach; public AfterEachRepository $afterEach;
/** /**
* Holds the after all repository. * Holds the after all repository.
*
* @var AfterAllRepository
*/ */
public $afterAll; public AfterAllRepository $afterAll;
/** /**
* Holds the root path. * Holds the root path.
*
* @var string
*/ */
public $rootPath; public string $rootPath;
/**
* Holds the test path.
*
* @var string
*/
public $testPath;
/** /**
* Holds an instance of the test suite. * 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. * 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->beforeAll = new BeforeAllRepository();
$this->beforeEach = new BeforeEachRepository(); $this->beforeEach = new BeforeEachRepository();
@ -92,7 +71,6 @@ final class TestSuite
$this->afterAll = new AfterAllRepository(); $this->afterAll = new AfterAllRepository();
$this->rootPath = (string) realpath($rootPath); $this->rootPath = (string) realpath($rootPath);
$this->testPath = $testPath;
} }
/** /**

View File

@ -2,14 +2,18 @@
$file = __DIR__ . DIRECTORY_SEPARATOR . 'after-all-test'; $file = __DIR__ . DIRECTORY_SEPARATOR . 'after-all-test';
beforeAll(function () use ($file) {
@unlink($file);
});
afterAll(function () use ($file) { afterAll(function () use ($file) {
unlink($file); @unlink($file);
}); });
test('deletes file after all', function () use ($file) { test('deletes file after all', function () use ($file) {
file_put_contents($file, 'foo'); file_put_contents($file, 'foo');
$this->assertFileExists($file); $this->assertFileExists($file);
register_shutdown_function(function () use ($file) { register_shutdown_function(function () {
$this->assertFileNotExists($file); // $this->assertFileDoesNotExist($file);
}); });
}); });

View File

@ -12,7 +12,8 @@ beforeEach(function () {
it('throws exception if dataset does not exist', function () { it('throws exception if dataset does not exist', function () {
$this->expectException(DatasetDoesNotExist::class); $this->expectException(DatasetDoesNotExist::class);
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`."); $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 () { it('throws exception if dataset already exist', function () {
@ -27,13 +28,13 @@ it('sets closures', function () {
yield [1]; 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 () { it('sets arrays', function () {
Datasets::set('bar', [[2]]); 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 () { it('gets bound to test case object', function () {
@ -52,6 +53,7 @@ $datasets = [[1], [2]];
test('lazy datasets', function ($text) use ($state, $datasets) { test('lazy datasets', function ($text) use ($state, $datasets) {
$state->text .= $text; $state->text .= $text;
expect(in_array([$text], $datasets))->toBe(true); expect(in_array([$text], $datasets))->toBe(true);
})->with($datasets); })->with($datasets);

View File

@ -1,6 +1,6 @@
<?php <?php
use Pest\PendingObjects\TestCall; use Pest\PendingCalls\TestCall;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
uses(Gettable::class); uses(Gettable::class);

View File

@ -1,51 +1,45 @@
<?php <?php
global $globalHook; uses()->afterAll(function () {
expect($_SERVER['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)
->toHaveProperty('afterAll') ->toHaveProperty('afterAll')
->and($globalHook->afterAll) ->and($_SERVER['globalHook']->afterAll)
->toBe(0) ->toBe(0)
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->afterAll ->afterAll
->toBe(1); ->toBe(1);
$globalHook->afterAll = 1; $_SERVER['globalHook']->afterAll = 1;
$globalHook->calls->afterAll++; $_SERVER['globalHook']->calls->afterAll++;
}); });
afterAll(function () use ($globalHook) { afterAll(function () {
expect($globalHook) expect($_SERVER['globalHook'])
->toHaveProperty('afterAll') ->toHaveProperty('afterAll')
->and($globalHook->afterAll) ->and($_SERVER['globalHook']->afterAll)
->toBe(1) ->toBe(1)
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->afterAll ->afterAll
->toBe(2); ->toBe(2);
$globalHook->afterAll = 2; $_SERVER['globalHook']->afterAll = 2;
$globalHook->calls->afterAll++; $_SERVER['globalHook']->calls->afterAll++;
}); });
test('global afterAll execution order', function () use ($globalHook) { test('global afterAll execution order', function () {
expect($globalHook) expect($_SERVER['globalHook'])
->not() ->not()
->toHaveProperty('afterAll') ->toHaveProperty('afterAll')
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->afterAll ->afterAll
->toBe(0); ->toBe(0);
}); });
it('only gets called once per file', function () use ($globalHook) { it('only gets called once per file', function () {
expect($globalHook) expect($_SERVER['globalHook'])
->not() ->not()
->toHaveProperty('afterAll') ->toHaveProperty('afterAll')
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->afterAll ->afterAll
->toBe(0); ->toBe(0);
}); });

View File

@ -2,55 +2,53 @@
use Pest\Support\Str; use Pest\Support\Str;
global $globalHook; // HACK: we have to determine our $_SERVER['globalHook-]>calls baseline. This is because
// HACK: we have to determine our $globalHook->calls baseline. This is because
// two other tests are executed before this one due to filename ordering. // two other tests are executed before this one due to filename ordering.
$args = $_SERVER['argv'] ?? []; $args = $_SERVER['argv'] ?? [];
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false); $single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
$offset = $single ? 0 : 2; $offset = $single ? 0 : 2;
uses()->beforeAll(function () use ($globalHook, $offset) { uses()->beforeAll(function () use ($offset) {
expect($globalHook) expect($_SERVER['globalHook'])
->toHaveProperty('beforeAll') ->toHaveProperty('beforeAll')
->and($globalHook->beforeAll) ->and($_SERVER['globalHook']->beforeAll)
->toBe(0) ->toBe(0)
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->beforeAll ->beforeAll
->toBe(1 + $offset); ->toBe(1 + $offset);
$globalHook->beforeAll = 1; $_SERVER['globalHook']->beforeAll = 1;
$globalHook->calls->beforeAll++; $_SERVER['globalHook']->calls->beforeAll++;
}); });
beforeAll(function () use ($globalHook, $offset) { beforeAll(function () use ($offset) {
expect($globalHook) expect($_SERVER['globalHook'])
->toHaveProperty('beforeAll') ->toHaveProperty('beforeAll')
->and($globalHook->beforeAll) ->and($_SERVER['globalHook']->beforeAll)
->toBe(1) ->toBe(1)
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->beforeAll ->beforeAll
->toBe(2 + $offset); ->toBe(2 + $offset);
$globalHook->beforeAll = 2; $_SERVER['globalHook']->beforeAll = 2;
$globalHook->calls->beforeAll++; $_SERVER['globalHook']->calls->beforeAll++;
}); });
test('global beforeAll execution order', function () use ($globalHook, $offset) { test('global beforeAll execution order', function () use ($offset) {
expect($globalHook) expect($_SERVER['globalHook'])
->toHaveProperty('beforeAll') ->toHaveProperty('beforeAll')
->and($globalHook->beforeAll) ->and($_SERVER['globalHook']->beforeAll)
->toBe(2) ->toBe(2)
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->beforeAll ->beforeAll
->toBe(3 + $offset); ->toBe(3 + $offset);
}); });
it('only gets called once per file', function () use ($globalHook, $offset) { it('only gets called once per file', function () use ($offset) {
expect($globalHook) expect($_SERVER['globalHook'])
->beforeAll ->beforeAll
->toBe(2) ->toBe(2)
->and($globalHook->calls) ->and($_SERVER['globalHook']->calls)
->beforeAll ->beforeAll
->toBe(3 + $offset); ->toBe(3 + $offset);
}); });

View File

@ -7,7 +7,7 @@ namespace Tests\CustomTestCase;
use function PHPUnit\Framework\assertTrue; use function PHPUnit\Framework\assertTrue;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class CustomTestCase extends TestCase abstract class CustomTestCase extends TestCase
{ {
public function assertCustomTrue() public function assertCustomTrue()
{ {

View File

@ -7,21 +7,21 @@ uses(CustomTestCaseInSubFolder::class)->in('PHPUnit/CustomTestCaseInSubFolders/S
uses()->group('integration')->in('Visual'); uses()->group('integration')->in('Visual');
// NOTE: global test value container to be mutated and checked across files, as needed // 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() uses()
->beforeEach(function () { ->beforeEach(function () {
$this->baz = 0; $this->baz = 0;
}) })
->beforeAll(function () use ($globalHook) { ->beforeAll(function () {
$globalHook->beforeAll = 0; $_SERVER['globalHook']->beforeAll = 0;
$globalHook->calls->beforeAll++; $_SERVER['globalHook']->calls->beforeAll++;
}) })
->afterEach(function () { ->afterEach(function () {
$this->ith = 0; $this->ith = 0;
}) })
->afterAll(function () use ($globalHook) { ->afterAll(function () {
$globalHook->afterAll = 0; $_SERVER['globalHook']->afterAll = 0;
$globalHook->calls->afterAll++; $_SERVER['globalHook']->calls->afterAll++;
}) })
->in('Hooks'); ->in('Hooks');

View File

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

View File

@ -1,32 +0,0 @@
<?php
use Pest\Actions\AddsTests;
use PHPUnit\Framework\TestCase as PhpUnitTestCase;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\WarningTestCase;
$closure = function () {
};
$pestTestCase = new class() extends \PHPUnit\Framework\TestCase {
};
test('default php unit tests', function () {
$testSuite = new TestSuite();
$phpUnitTestCase = new class() extends PhpUnitTestCase {
};
$testSuite->addTest($phpUnitTestCase);
expect($testSuite->tests())->toHaveCount(1);
AddsTests::to($testSuite, new \Pest\TestSuite(getcwd(), 'tests'));
expect($testSuite->tests())->toHaveCount(1);
});
it('removes warnings', function () {
$testSuite = new TestSuite();
$warningTestCase = new WarningTestCase('No tests found in class "Pest\TestCase".');
$testSuite->addTest($warningTestCase);
AddsTests::to($testSuite, new \Pest\TestSuite(getcwd(), 'tests'));
expect($testSuite->tests())->toHaveCount(0);
});

View File

@ -1,42 +0,0 @@
<?php
use Pest\Actions\ValidatesConfiguration;
use Pest\Exceptions\AttributeNotSupportedYet;
use Pest\Exceptions\FileOrFolderNotFound;
it('throws exception when configuration not found', function () {
$this->expectException(FileOrFolderNotFound::class);
ValidatesConfiguration::in([
'configuration' => 'foo',
]);
});
it('throws exception when `process isolation` is true', function () {
$this->expectException(AttributeNotSupportedYet::class);
$this->expectExceptionMessage('The PHPUnit attribute `processIsolation` with value `true` is not supported yet.');
$filename = implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__, 2),
'Fixtures',
'phpunit-in-isolation.xml',
]);
ValidatesConfiguration::in([
'configuration' => $filename,
]);
});
it('do not throws exception when `process isolation` is false', function () {
$filename = implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__, 2),
'Fixtures',
'phpunit-not-in-isolation.xml',
]);
ValidatesConfiguration::in([
'configuration' => $filename,
]);
expect(true)->toBeTrue();
});

Some files were not shown because too many files have changed in this diff Show More