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:
matrix:
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]
parallel: ['', '--parallel']
exclude:

View File

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

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

View File

@ -17,16 +17,21 @@
}
],
"require": {
"php": "^7.3 || ^8.0",
"nunomaduro/collision": "^5.4.0|^6.0",
"php": "^8.0",
"nunomaduro/collision": "^5.10.0|^6.0",
"pestphp/pest-plugin": "^1.0.0",
"phpunit/phpunit": "^9.5.5"
"phpunit/phpunit": "10.0.x-dev"
},
"autoload": {
"psr-4": {
"Pest\\": "src/"
},
"exclude-from-classmap": [
"../phpunit/src/Runner/TestSuiteLoader.php",
"vendor/phpunit/phpunit/src/Runner/TestSuiteLoader.php"
],
"files": [
"overrides/Runner/TestSuiteLoader.php",
"src/Functions.php",
"src/Pest.php"
]
@ -43,8 +48,7 @@
"illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master",
"pestphp/pest-plugin-parallel": "^1.0"
"pestphp/pest-dev-tools": "dev-master"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -73,7 +77,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-next": "2.x-dev"
},
"pest": {
"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);
namespace Pest\Actions;
namespace Pest\Bootstrappers;
use Pest\Support\Str;
use function Pest\testDirectory;
use PHPUnit\Util\FileLoader;
use Pest\TestSuite;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* @internal
*/
final class LoadStructure
final class BootFiles
{
/**
* The Pest convention.
@ -21,24 +21,23 @@ final class LoadStructure
* @var array<int, string>
*/
private const STRUCTURE = [
'Expectations.php',
'Datasets',
'Datasets.php',
'Expectations',
'Expectations.php',
'Helpers',
'Helpers.php',
'Pest.php',
'Datasets',
];
/**
* Validates the configuration in the given `configuration`.
* Boots the Subscribers.
*/
public static function in(string $rootPath): void
public function __invoke(): void
{
$rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
$load = function ($filename): bool {
return file_exists($filename) && (bool) FileLoader::checkAndLoad($filename);
};
foreach (self::STRUCTURE as $filename) {
$filename = sprintf('%s%s%s', $testsPath, DIRECTORY_SEPARATOR, $filename);
@ -50,14 +49,21 @@ final class LoadStructure
$directory = new RecursiveDirectoryIterator($filename);
$iterator = new RecursiveIteratorIterator($directory);
foreach ($iterator as $file) {
$filename = $file->__toString();
if (Str::endsWith($filename, '.php') && file_exists($filename)) {
require_once $filename;
}
$this->load($file->__toString());
}
} else {
$load($filename);
$this->load($filename);
}
}
}
/**
* Loads the given filename, if possible.
*/
private function load(string $filename): void
{
if (Str::endsWith($filename, '.php') && file_exists($filename)) {
include_once $filename;
}
}
}

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
*
* Creates a new expectation.
* Creates a new Expectation.
*
* @param TValue $value
*
* @return Expectation<TValue>
*/
public function expect($value): Expectation
public function expect(mixed $value): Expectation
{
return new Expectation($value);
}

View File

@ -13,12 +13,14 @@ use Closure;
trait Extendable
{
/**
* The list of extends.
*
* @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
{
@ -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
{
@ -37,10 +39,8 @@ trait Extendable
* Dynamically handle calls to the class.
*
* @param array<int, mixed> $parameters
*
* @return mixed
*/
public function __call(string $method, array $parameters)
public function __call(string $method, array $parameters): mixed
{
if (!static::hasExtend($method)) {
throw new BadMethodCallException("$method is not a callable method name.");

View File

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

View File

@ -19,7 +19,7 @@ trait RetrievesValues
*
* @return TRetrievableValue|null
*/
private function retrieve(string $key, $value, $default = null)
private function retrieve(string $key, mixed $value, mixed $default = null): mixed
{
if (is_array($value)) {
return $value[$key] ?? $default;

View File

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

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ namespace Pest\Contracts\Plugins;
interface AddsOutput
{
/**
* Allows to add custom output after the test suite was executed.
* Adds output after the Test Suite execution.
*/
public function addOutput(int $testReturnCode): int;
public function addOutput(int $exitCode): int;
}

View File

@ -10,11 +10,11 @@ namespace Pest\Contracts\Plugins;
interface HandlesArguments
{
/**
* Allows to handle custom command line arguments.
* Adds arguments before of the Test Suite execution.
*
* @param array<int, string> $arguments
* @param array<int, string> $argv
*
* @return array<int, string> the updated list of arguments
* @return array<int, string>
*/
public function handleArguments(array $arguments): array;
public function handleArguments(array $argv): array;
}

View File

@ -20,14 +20,21 @@ final class Datasets
*
* @var array<int|string, Closure|iterable<int|string, mixed>>
*/
private static $datasets = [];
private static array $datasets = [];
/**
* Holds the withs.
*
* @var array<string, \Closure|iterable|string>
*/
private static array $withs = [];
/**
* Sets the given.
*
* @param Closure|iterable<int|string, mixed> $data
*/
public static function set(string $name, $data): void
public static function set(string $name, Closure|iterable $data): void
{
if (array_key_exists($name, self::$datasets)) {
throw new DatasetAlreadyExist($name);
@ -36,35 +43,43 @@ final class Datasets
self::$datasets[$name] = $data;
}
/**
* Sets the given.
*
* @param Closure|iterable<int|string, mixed> $data
*/
public static function with(string $filename, string $description, Closure|iterable|string $with): void
{
self::$withs[$filename . '>>>' . $description] = $with;
}
/**
* @return Closure|iterable<int|string, mixed>
*/
public static function get(string $name)
public static function get(string $filename, string $description): Closure|iterable
{
if (!array_key_exists($name, self::$datasets)) {
throw new DatasetDoesNotExist($name);
}
$dataset = self::$withs[$filename . '>>>' . $description];
return self::$datasets[$name];
return self::resolve($description, $dataset);
}
/**
* Resolves the current dataset to an array value.
*
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
* @param array<Closure|iterable<int|string, mixed>|string> $dataset
*
* @return array<string, mixed>
* @return array<string, mixed>|null
*/
public static function resolve(string $description, array $datasets): array
public static function resolve(string $description, array $dataset): array|null
{
/* @phpstan-ignore-next-line */
if (empty($datasets)) {
return [$description => []];
if (empty($dataset)) {
return null;
}
$datasets = self::processDatasets($datasets);
$dataset = self::processDatasets($dataset);
$datasetCombinations = self::getDataSetsCombinations($datasets);
$datasetCombinations = self::getDataSetsCombinations($dataset);
$dataSetDescriptions = [];
$dataSetValues = [];
@ -114,7 +129,11 @@ final class Datasets
$processedDataset = [];
if (is_string($data)) {
$datasets[$index] = self::get($data);
if (!isset(self::$datasets[$data])) {
throw new DatasetDoesNotExist($data);
}
$datasets[$index] = self::$datasets[$data];
}
if (is_callable($datasets[$index])) {
@ -161,10 +180,9 @@ final class Datasets
}
/**
* @param int|string $key
* @param array<int, mixed> $data
*/
private static function getDataSetDescription($key, array $data): string
private static function getDataSetDescription(int|string $key, array $data): string
{
$exporter = new Exporter();

View File

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

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class AfterAllAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of after all already exist exception.
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class AfterEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of after each already exist exception.
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class AttributeNotSupportedYet extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of attribute not supported yet.
* Creates a new Exception instance.
*/
public function __construct(string $attribute, string $value)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class BeforeEachAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of before each already exist exception.
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class DatasetAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of dataset already exist.
* Creates a new Exception instance.
*/
public function __construct(string $name)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class DatasetDoesNotExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of dataset does not exist.
* Creates a new Exception instance.
*/
public function __construct(string $name)
{

View File

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

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class FileOrFolderNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of file not found.
* Creates a new Exception instance.
*/
public function __construct(string $filename)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class InvalidConsoleArgument extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of should not happen.
* Creates a new Exception instance.
*/
public function __construct(string $message)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class InvalidPestCommand extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of invalid pest command exception.
* Creates a new Exception instance.
*/
public function __construct()
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class MissingDependency extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of missing dependency.
* Creates a new Exception instance.
*/
public function __construct(string $feature, string $dependency)
{

View File

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

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class TestAlreadyExist extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of test already exist.
* Creates a new Exception instance.
*/
public function __construct(string $fileName, string $description)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class TestCaseAlreadyInUse extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of test case already in use.
* Creates a new Exception instance.
*/
public function __construct(string $inUse, string $newOne, string $folder)
{

View File

@ -15,7 +15,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
{
/**
* Creates a new instance of after each already exist exception.
* Creates a new Exception instance.
*/
public function __construct(string $testCaseClass)
{

View File

@ -29,37 +29,26 @@ use Throwable;
*/
final class Expectation
{
use Extendable {
use RetrievesValues, Extendable {
__call as __extendsCall;
}
use RetrievesValues;
/**
* The expectation value.
*
* @readonly
*
* @var mixed
*/
public $value;
/**
* The exporter instance, if any.
*
* @readonly
*
* @var Exporter|null
*/
private $exporter;
private ?Exporter $exporter = null;
/**
* Creates a new expectation.
*
* @param TValue $value
*/
public function __construct($value)
{
$this->value = $value;
public function __construct(
public mixed $value
) {
// ..
}
/**
@ -69,7 +58,7 @@ final class Expectation
*
* @return Expectation<TValue>
*/
public function and($value): Expectation
public function and(mixed $value): Expectation
{
return new self($value);
}
@ -103,9 +92,9 @@ final class Expectation
/**
* 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')) {
// @phpstan-ignore-next-line
@ -146,9 +135,9 @@ final class Expectation
*
* @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)) {
throw new BadMethodCallException('Expectation value is not iterable.');
@ -187,16 +176,14 @@ final class Expectation
*
* @template TMatchSubject of array-key
*
* @param callable(): TMatchSubject|TMatchSubject $subject
* @param (callable(): TMatchSubject)|TMatchSubject $subject
* @param array<TMatchSubject, (callable(Expectation<TValue>): mixed)|TValue> $expressions
*/
public function match($subject, array $expressions): Expectation
public function match(mixed $subject, array $expressions): Expectation
{
$subject = is_callable($subject)
? $subject
: function () use ($subject) {
return $subject;
};
: fn () => $subject;
$subject = $subject();
@ -229,15 +216,15 @@ final class Expectation
/**
* Apply the callback if the given "condition" is falsy.
*
* @param (callable(): bool)|bool $condition
* @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback
*/
public function unless($condition, callable $callback): Expectation
public function unless(callable|bool $condition, callable $callback): Expectation
{
$condition = is_callable($condition)
? $condition
: static function () use ($condition): bool {
return (bool) $condition; // @phpstan-ignore-line
return $condition; // @phpstan-ignore-line
};
return $this->when(!$condition(), $callback);
@ -246,15 +233,15 @@ final class Expectation
/**
* Apply the callback if the given "condition" is truthy.
*
* @param (callable(): bool)|bool $condition
* @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback
*/
public function when($condition, callable $callback): Expectation
public function when(callable|bool $condition, callable $callback): Expectation
{
$condition = is_callable($condition)
? $condition
: static function () use ($condition): bool {
return (bool) $condition; // @phpstan-ignore-line
return $condition; // @phpstan-ignore-line
};
if ($condition()) {
@ -268,10 +255,8 @@ final class Expectation
* Asserts that two variables have the same type and
* value. Used on objects, it asserts that two
* variables reference the same object.
*
* @param mixed $expected
*/
public function toBe($expected): Expectation
public function toBe(mixed $expected): Expectation
{
Assert::assertSame($expected, $this->value);
@ -330,10 +315,8 @@ final class Expectation
/**
* 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);
@ -342,10 +325,8 @@ final class Expectation
/**
* 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);
@ -354,10 +335,8 @@ final class Expectation
/**
* 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);
@ -366,10 +345,8 @@ final class Expectation
/**
* 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);
@ -378,10 +355,8 @@ final class Expectation
/**
* 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) {
if (is_string($this->value)) {
@ -456,10 +431,8 @@ final class Expectation
/**
* 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();
@ -489,10 +462,8 @@ final class Expectation
/**
* 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);
@ -507,10 +478,8 @@ final class Expectation
* are sorted before they are compared. When $expected and $this->value
* are objects, each object is converted to an array containing all
* private, protected and public attributes.
*
* @param mixed $expected
*/
public function toEqualCanonicalizing($expected): Expectation
public function toEqualCanonicalizing(mixed $expected): Expectation
{
Assert::assertEqualsCanonicalizing($expected, $this->value);
@ -520,10 +489,8 @@ final class Expectation
/**
* Asserts that the absolute difference between the value and $expected
* 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);
@ -555,9 +522,9 @@ final class Expectation
/**
* 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 */
Assert::assertInstanceOf($class, $this->value);
@ -708,11 +675,8 @@ final class Expectation
/**
* 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')) {
$array = $this->value->toArray();
@ -812,9 +776,9 @@ final class Expectation
/**
* 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')) {
$valueAsArray = $this->value->toArray();
@ -843,9 +807,9 @@ final class Expectation
* Asserts that the value object matches a subset
* 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) {
Assert::assertTrue(property_exists($this->value, $property));
@ -891,7 +855,7 @@ final class Expectation
*
* @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();
@ -938,10 +902,8 @@ final class Expectation
/**
* Exports the given value.
*
* @param mixed $value
*/
private function export($value): string
private function export(mixed $value): string
{
if ($this->exporter === null) {
$this->exporter = new Exporter();
@ -971,10 +933,8 @@ final class Expectation
/**
* Dynamically calls methods on the class without any arguments
* or creates a new higher order expectation.
*
* @return Expectation|HigherOrderExpectation
*/
public function __get(string $name)
public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation
{
if (!method_exists($this, $name) && !static::hasExtend($name)) {
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;
use Closure;
use ParseError;
use Pest\Concerns;
use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Datasets;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Support\HigherOrderMessageCollection;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\Plugins\Environment;
use Pest\Support\Reflection;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use RuntimeException;
@ -22,159 +24,84 @@ use RuntimeException;
*/
final class TestCaseFactory
{
/**
* Holds the test filename.
*
* @readonly
*
* @var string
*/
public $filename;
use HigherOrderable;
/**
* Marks this test case as only.
* The list of annotations.
*
* @readonly
*
* @var bool
* @var array<int, class-string>
*/
public $only = false;
private static array $annotations = [
Annotations\Depends::class,
Annotations\Groups::class,
];
/**
* Holds the test description.
* The FQN of the Test Case class.
*
* If the description is null, means that it
* will be created with the given assertions.
*
* @var string|null
* @var class-string
*/
public $description;
public string $class = TestCase::class;
/**
* Holds the test closure.
* The list of class methods.
*
* @readonly
*
* @var Closure
* @var array<string, TestCaseMethodFactory>
*/
public $test;
public array $methods = [];
/**
* Holds the dataset, if any.
* The list of class traits.
*
* @var array<Closure|iterable<int|string, mixed>|string>
* @var array <int, class-string>
*/
public $datasets = [];
/**
* The FQN of the test case class.
*
* @var string
*/
public $class = TestCase::class;
/**
* An array of FQN of the class traits.
*
* @var array <int, string>
*/
public $traits = [
public array $traits = [
Concerns\Testable::class,
Concerns\Expectable::class,
];
/**
* Holds the higher order messages
* for the factory that are proxyble.
*
* @var HigherOrderMessageCollection
* Creates a new Factory instance.
*/
public $factoryProxies;
public function __construct(
public string $filename
) {
$this->bootHigherOrderable();
}
/**
* Holds the higher order
* messages that are proxyble.
*
* @var HigherOrderMessageCollection
*/
public $proxies;
/**
* Holds the higher order
* messages that are chainable.
*
* @var HigherOrderMessageCollection
*/
public $chains;
/**
* Creates a new anonymous test case pending object.
*/
public function __construct(string $filename, string $description = null, Closure $closure = null)
public function make(): void
{
$this->filename = $filename;
$this->description = $description;
$this->test = $closure ?? function (): void {
if (Assert::getCount() === 0) {
self::markTestIncomplete(); // @phpstan-ignore-line
}
};
$methods = array_filter($this->methods, function ($method) {
return count($onlyTestCases = $this->methodsUsingOnly()) === 0 || in_array($method, $onlyTestCases, true);
});
$this->factoryProxies = new HigherOrderMessageCollection();
$this->proxies = new HigherOrderMessageCollection();
$this->chains = new HigherOrderMessageCollection();
if (count($this->methods) > 0) {
$this->evaluate($this->filename, $methods);
}
}
/**
* Builds the anonymous test case.
* Returns all the "only" methods.
*
* @return array<int, TestCase>
* @return array<int, TestCaseMethodFactory>
*/
public function build(TestSuite $testSuite): array
public function methodsUsingOnly(): array
{
if ($this->description === null) {
throw ShouldNotHappen::fromMessage('Description can not be empty.');
if (Environment::name() === Environment::CI) {
return [];
}
$chains = $this->chains;
$proxies = $this->proxies;
$factoryTest = $this->test;
/**
* @return mixed
*/
$test = function () use ($chains, $proxies, $factoryTest) {
$proxies->proxy($this);
$chains->chain($this);
/* @phpstan-ignore-next-line */
return call_user_func(Closure::bind($factoryTest, $this, get_class($this)), ...func_get_args());
};
$className = $this->makeClassFromFilename($this->filename);
$createTest = function ($description, $data) use ($className, $test) {
$testCase = new $className($test, $description, $data);
$this->factoryProxies->proxy($testCase);
return $testCase;
};
$datasets = Datasets::resolve($this->description, $this->datasets);
return array_map($createTest, array_keys($datasets), $datasets);
return array_filter($this->methods, static fn ($method): bool => $method->only);
}
/**
* Makes a fully qualified class name from the given filename.
* Creates a Test Case class using a runtime evaluate.
*/
public function makeClassFromFilename(string $filename): string
public function evaluate(string $filename, array $methods): string
{
if ('\\' === DIRECTORY_SEPARATOR) {
// In case Windows, strtolower drive name, like in UsesCall.
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', function ($match): string {
return strtolower($match['drive']);
}, $filename);
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $filename);
}
$filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
@ -186,9 +113,7 @@ final class TestCaseFactory
// Strip out any %-encoded octets.
$relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath);
// Remove escaped quote sequences (maintain namespace)
$relativePath = str_replace(array_map(function (string $quote): string {
return sprintf('\\%s', $quote);
}, ['\'', '"']), '', $relativePath);
$relativePath = str_replace(array_map(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath);
// Limit to A-Z, a-z, 0-9, '_', '-'.
$relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath);
@ -198,9 +123,9 @@ final class TestCaseFactory
}
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
$traitsCode = sprintf('use %s;', implode(', ', array_map(function ($trait): string {
return sprintf('\%s', $trait);
}, $this->traits)));
$traitsCode = sprintf('use %s;', implode(', ', array_map(
static fn ($trait): string => sprintf('\%s', $trait), $this->traits))
);
$partsFQN = explode('\\', $classFQN);
$className = array_pop($partsFQN);
@ -212,14 +137,65 @@ final class TestCaseFactory
$classFQN .= $className;
}
$methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string {
$methodName = Str::evaluable($method->description);
$datasetsCode = '';
$annotations = ['@test'];
foreach (self::$annotations as $annotation) {
$annotations = (new $annotation())->add($method, $annotations);
}
if (!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 {
eval("
namespace $namespace;
use Pest\Datasets as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode
private static \$__filename = '$filename';
$methodsCode
}
");
} catch (ParseError $caught) {
@ -230,11 +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
|| $this->factoryProxies->count('addDependencies') > 0;
if ($method->description === null) {
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\Expectation;
use Pest\PendingObjects\AfterEachCall;
use Pest\PendingObjects\BeforeEachCall;
use Pest\PendingObjects\TestCall;
use Pest\PendingObjects\UsesCall;
use Pest\PendingCalls\AfterEachCall;
use Pest\PendingCalls\BeforeEachCall;
use Pest\PendingCalls\TestCall;
use Pest\PendingCalls\UsesCall;
use Pest\Support\Backtrace;
use Pest\Support\Extendable;
use Pest\Support\HigherOrderTapProxy;
@ -18,10 +18,8 @@ use PHPUnit\Framework\TestCase;
* Creates a new expectation.
*
* @param mixed $value the Value
*
* @return Expectation|Extendable
*/
function expect($value = null)
function expect($value = null): Expectation|Extendable
{
if (func_num_args() === 0) {
return new Extendable(Expectation::class);
@ -60,7 +58,7 @@ if (!function_exists('dataset')) {
*
* @param Closure|iterable<int|string, mixed> $dataset
*/
function dataset(string $name, $dataset): void
function dataset(string $name, Closure|iterable $dataset): void
{
Datasets::set($name, $dataset);
}

View File

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

15
src/IgnorableTestCase.php Normal file
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
{
/**
* The console command name.
* The Console Command name.
*
* @var string
*/
@ -25,7 +25,7 @@ final class PestDatasetCommand extends Command
{--test-directory=tests : The name of the tests directory}';
/**
* The console command description.
* The Console Command description.
*
* @var string
*/

View File

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

View File

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

View File

@ -16,9 +16,9 @@ use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\DefaultResultPrinter;
use PHPUnit\TextUI\XmlConfiguration\Logging\TeamCity as BaseTeamCity;
use function round;
use function str_replace;
use function strlen;
use Throwable;
final class TeamCity extends DefaultResultPrinter
@ -34,22 +34,19 @@ final class TeamCity extends DefaultResultPrinter
private const TEST_STARTED = 'testStarted';
private const TEST_FINISHED = 'testFinished';
/** @var int */
private $flowId;
private ?int $flowId = null;
/** @var bool */
private $isSummaryTestCountPrinted = false;
private bool $isSummaryTestCountPrinted = false;
/** @var \PHPUnit\Util\Log\TeamCity */
private $phpunitTeamCity;
private BaseTeamCity $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);
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity($out, $verbose, $colors);
$this->phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors);
$this->logo();
}
@ -74,9 +71,7 @@ final class TeamCity extends DefaultResultPrinter
'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'],
];
$filteredResults = array_filter($results, function ($item): bool {
return $item['count'] > 0;
});
$filteredResults = array_filter($results, fn ($item): bool => $item['count'] > 0);
foreach ($filteredResults as $key => $info) {
$this->writeWithColor($info['color'], $info['count'] . " $key", false);
@ -203,7 +198,7 @@ final class TeamCity extends DefaultResultPrinter
*/
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
{
/**
* @var Expectation
*/
private $original;
/**
* Creates a new opposite expectation.
*/
public function __construct(Expectation $original)
public function __construct(private Expectation $original)
{
$this->original = $original;
// ..
}
/**
@ -37,7 +32,7 @@ final class OppositeExpectation
foreach ($keys as $key) {
try {
$this->original->toHaveKey($key);
} catch (ExpectationFailedException $e) {
} catch (ExpectationFailedException) {
continue;
}
@ -57,7 +52,7 @@ final class OppositeExpectation
try {
/* @phpstan-ignore-next-line */
$this->original->{$name}(...$arguments);
} catch (ExpectationFailedException $e) {
} catch (ExpectationFailedException) {
return $this->original;
}
@ -73,7 +68,7 @@ final class OppositeExpectation
try {
/* @phpstan-ignore-next-line */
$this->original->{$name};
} catch (ExpectationFailedException $e) {
} catch (ExpectationFailedException) {
return $this->original;
}
@ -90,10 +85,8 @@ final class OppositeExpectation
{
$exporter = new Exporter();
$toString = function ($argument) use ($exporter): string {
return $exporter->shortenedExport($argument);
};
$toString = fn ($argument): string => $exporter->shortenedExport($argument);
throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)), implode(' ', array_map(function ($argument) use ($toString): string { return $toString($argument); }, $arguments))));
throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(?<!\ )[A-Z]/', ' $0', $name)), implode(' ', array_map(fn ($argument): string => $toString($argument), $arguments))));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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';
/**
* Whether should show the coverage or not.
*
* @var bool
* Whether it should show the coverage or not.
*/
public $coverage = false;
public bool $coverage = false;
/**
* The minimum coverage.
*
* @var float
*/
public $coverageMin = 0.0;
public float $coverageMin = 0.0;
/**
* @var OutputInterface
* Creates a new Plugin instance.
*/
private $output;
public function __construct(OutputInterface $output)
public function __construct(private OutputInterface $output)
{
$this->output = $output;
// ..
}
/**
* @param array<int, string> $originals
*
* @return array<int, string>
*/
public function handleArguments(array $originals): array
{
$arguments = array_merge([''], array_values(array_filter($originals, function ($original): bool {
$arguments = [...[''], ...array_values(array_filter($originals, function ($original): bool {
foreach ([self::COVERAGE_OPTION, self::MIN_OPTION] as $option) {
if ($original === sprintf('--%s', $option) || Str::startsWith($original, sprintf('--%s=', $option))) {
return true;
@ -61,7 +60,7 @@ final class Coverage implements AddsOutput, HandlesArguments
}
return false;
})));
}))];
$originals = array_flip($originals);
foreach ($arguments as $argument) {
@ -75,9 +74,9 @@ final class Coverage implements AddsOutput, HandlesArguments
$input = new ArgvInput($arguments, new InputDefinition($inputs));
if ((bool) $input->getOption(self::COVERAGE_OPTION)) {
$this->coverage = true;
$originals[] = '--coverage-php';
$originals[] = \Pest\Support\Coverage::getPath();
$this->coverage = true;
$originals[] = '--coverage-php';
$originals[] = \Pest\Support\Coverage::getPath();
}
if ($input->getOption(self::MIN_OPTION) !== null) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,16 +5,11 @@ declare(strict_types=1);
namespace Pest\Repositories;
use Closure;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\TestCaseFactory;
use Pest\Plugins\Environment;
use Pest\Support\Reflection;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase;
/**
@ -22,27 +17,22 @@ use PHPUnit\Framework\TestCase;
*/
final class TestRepository
{
/**
* @var non-empty-string
*/
private const SEPARATOR = '>>>';
/**
* @var array<string, TestCaseFactory>
*/
private $state = [];
private array $testCases = [];
/**
* @var array<string, array<int, array<int, string|Closure>>>
*/
private $uses = [];
private array $uses = [];
/**
* Counts the number of test cases.
*/
public function count(): int
{
return count($this->state);
return count($this->testCases);
}
/**
@ -52,81 +42,13 @@ final class TestRepository
*/
public function getFilenames(): array
{
$testsWithOnly = $this->testsUsingOnly();
$testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase) => count($testCase->methodsUsingOnly()) > 0);
return array_values(array_map(function (TestCaseFactory $factory): string {
return $factory->filename;
}, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
}
/**
* 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);
}
if (count($testCases) === 0) {
$testCases = $this->testCases;
}
$onlyState = $this->testsUsingOnly();
$state = count($onlyState) > 0 ? $onlyState : $this->state;
foreach ($state as $testFactory) {
/** @var TestCaseFactory $testFactory */
$tests = $testFactory->build($testSuite);
foreach ($tests as $test) {
$each($test);
}
}
}
/**
* Return all tests that have called the only method.
*
* @return array<TestCaseFactory>
*/
private function testsUsingOnly(): array
{
if (Environment::name() === Environment::CI) {
return [];
}
return array_filter($this->state, function ($testFactory): bool {
return $testFactory->only;
});
return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $testCases));
}
/**
@ -148,8 +70,8 @@ final class TestRepository
foreach ($paths as $path) {
if (array_key_exists($path, $this->uses)) {
$this->uses[$path] = [
array_merge($this->uses[$path][0], $classOrTraits),
array_merge($this->uses[$path][1], $groups),
[...$this->uses[$path][0], ...$classOrTraits],
[...$this->uses[$path][1], ...$groups],
$this->uses[$path][2] + $hooks, // NOTE: array_merge will destroy numeric indices
];
} else {
@ -158,27 +80,73 @@ final class TestRepository
}
}
/**
* Sets a test case by the given filename and description.
*/
public function set(TestCaseFactory $test): void
public function get($filename): TestCaseFactory
{
if ($test->description === null) {
throw ShouldNotHappen::fromMessage('Trying to create a test without description.');
return $this->testCases[$filename];
}
/**
* 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 (array_key_exists(sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description), $this->state)) {
throw new TestAlreadyExist($test->filename, $test->description);
$this->testCases[$method->filename]->addMethod($method);
}
/**
* 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]);
}
}
if (!$test->receivesArguments()) {
$arguments = Reflection::getFunctionArguments($test->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);
if (count($arguments) > 0) {
throw new DatasetMissing($test->filename, $test->description, $arguments);
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]);
}
}
$this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test;
$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;
/**
* Credits: most of this class methods and implementations
* belongs to the Arr helper of laravel/framework project
* (https://github.com/laravel/framework).
*
* @internal
*/
final class Arr
{
/**
* @param array<mixed> $array
* @param string|int $key
* Checks if the given array has the given key.
*/
public static function has(array $array, $key): bool
public static function has(array $array, string|int $key): bool
{
$key = (string) $key;
@ -37,13 +32,9 @@ final class Arr
}
/**
* @param array<mixed> $array
* @param string|int $key
* @param null $default
*
* @return array|mixed|null
* Gets the given key value.
*/
public static function get(array $array, $key, $default = null)
public static function get(array $array, string|int $key, mixed $default = null): mixed
{
$key = (string) $key;
@ -51,7 +42,7 @@ final class Arr
return $array[$key];
}
if (strpos($key, '.') === false) {
if (!str_contains($key, '.')) {
return $array[$key] ?? $default;
}

View File

@ -26,7 +26,7 @@ final class Backtrace
$current = null;
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
if (Str::endsWith($trace[self::FILE], (string) realpath('vendor/phpunit/phpunit/src/Util/FileLoader.php'))) {
if (Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) {
break;
}

View File

@ -18,9 +18,9 @@ final class ChainableClosure
{
return function () use ($closure, $next): void {
/* @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 */
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
{
/**
* @var self
*/
private static $instance;
private static ?Container $instance = null;
/**
* @var array<string, mixed>
*/
private $instances = [];
private array $instances = [];
/**
* Gets a new or already existing container.

View File

@ -162,7 +162,7 @@ final class Coverage
$lastKey = count($array) - 1;
if (array_key_exists($lastKey, $array) && strpos($array[$lastKey], '..') !== false) {
if (array_key_exists($lastKey, $array) && str_contains($array[$lastKey], '..')) {
[$from] = explode('..', $array[$lastKey]);
$array[$lastKey] = $line > $from ? sprintf('%s..%s', $from, $line) : sprintf('%s..%s', $line, $from);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ final class Str
*/
public static function startsWith(string $target, string $search): bool
{
return substr($target, 0, strlen($search)) === $search;
return str_starts_with($target, $search);
}
/**
@ -48,4 +48,14 @@ final class Str
return substr($target, -$length) === $search;
}
/**
* Makes the given string evaluable by an `eval`.
*/
public static function evaluable(string $code): string
{
$code = str_replace(' ', '_', $code);
return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,51 +1,45 @@
<?php
global $globalHook;
// NOTE: this test does not have a $globalHook->calls offset since it is first
// in the directory and thus will always run before the others. See also the
// BeforeAllTest.php for details.
uses()->afterAll(function () use ($globalHook) {
expect($globalHook)
uses()->afterAll(function () {
expect($_SERVER['globalHook'])
->toHaveProperty('afterAll')
->and($globalHook->afterAll)
->and($_SERVER['globalHook']->afterAll)
->toBe(0)
->and($globalHook->calls)
->and($_SERVER['globalHook']->calls)
->afterAll
->toBe(1);
$globalHook->afterAll = 1;
$globalHook->calls->afterAll++;
$_SERVER['globalHook']->afterAll = 1;
$_SERVER['globalHook']->calls->afterAll++;
});
afterAll(function () use ($globalHook) {
expect($globalHook)
afterAll(function () {
expect($_SERVER['globalHook'])
->toHaveProperty('afterAll')
->and($globalHook->afterAll)
->and($_SERVER['globalHook']->afterAll)
->toBe(1)
->and($globalHook->calls)
->and($_SERVER['globalHook']->calls)
->afterAll
->toBe(2);
$globalHook->afterAll = 2;
$globalHook->calls->afterAll++;
$_SERVER['globalHook']->afterAll = 2;
$_SERVER['globalHook']->calls->afterAll++;
});
test('global afterAll execution order', function () use ($globalHook) {
expect($globalHook)
test('global afterAll execution order', function () {
expect($_SERVER['globalHook'])
->not()
->toHaveProperty('afterAll')
->and($globalHook->calls)
->and($_SERVER['globalHook']->calls)
->afterAll
->toBe(0);
});
it('only gets called once per file', function () use ($globalHook) {
expect($globalHook)
it('only gets called once per file', function () {
expect($_SERVER['globalHook'])
->not()
->toHaveProperty('afterAll')
->and($globalHook->calls)
->and($_SERVER['globalHook']->calls)
->afterAll
->toBe(0);
});

View File

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

View File

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

View File

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

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