feat: reworks evalution of Test Case

This commit is contained in:
Nuno Maduro
2021-11-14 19:58:25 +00:00
parent cd34f0ba81
commit 4b213d63bd
26 changed files with 603 additions and 620 deletions

View File

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

View File

@ -48,8 +48,7 @@
"illuminate/console": "^8.47.0", "illuminate/console": "^8.47.0",
"illuminate/support": "^8.47.0", "illuminate/support": "^8.47.0",
"laravel/dusk": "^6.15.0", "laravel/dusk": "^6.15.0",
"pestphp/pest-dev-tools": "dev-master", "pestphp/pest-dev-tools": "dev-master"
"pestphp/pest-plugin-mock": "^1.0"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,

View File

@ -1,22 +1,5 @@
<?php <?php
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 function stripos;
use function strlen;
use function substr;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionException;
use PHPUnit\Framework\WarningTestCase;
/** /**
* Copyright (c) 2001-2021, Sebastian Bergmann <sebastian@phpunit.de>. * Copyright (c) 2001-2021, Sebastian Bergmann <sebastian@phpunit.de>.
* All rights reserved. * All rights reserved.
@ -51,35 +34,82 @@ use PHPUnit\Framework\WarningTestCase;
* POSSIBILITY OF SUCH DAMAGE. * 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 final class TestSuiteLoader
{ {
/** /**
* Loads the test suite. * @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 public function load(string $suiteClassFile): ReflectionClass
{ {
$suiteClassName = basename($suiteClassFile, '.php'); $suiteClassName = $this->classNameFromFileName($suiteClassFile);
$loadedClasses = get_declared_classes();
if (!class_exists($suiteClassName, false)) { if (!class_exists($suiteClassName, false)) {
(static function () use ($suiteClassFile) { (static function () use ($suiteClassFile) {
include_once $suiteClassFile; include_once $suiteClassFile;
TestSuite::getInstance()->tests->makeIfExists($suiteClassFile);
})(); })();
$loadedClasses = array_values( $loadedClasses = array_values(
array_diff(get_declared_classes(), $loadedClasses) array_diff(
get_declared_classes(),
array_merge(
self::$declaredClasses,
self::$loadedClasses
)
)
); );
if (empty($loadedClasses)) { self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses);
return new ReflectionClass(WarningTestCase::class);
if (empty(self::$loadedClasses)) {
return $this->exceptionFor($suiteClassName, $suiteClassFile);
} }
} }
if (!class_exists($suiteClassName, false)) { if (!class_exists($suiteClassName, false)) {
// this block will handle namespaced classes
$offset = 0 - strlen($suiteClassName); $offset = 0 - strlen($suiteClassName);
foreach ($loadedClasses as $loadedClass) { foreach (self::$loadedClasses as $loadedClass) {
if (stripos(substr($loadedClass, $offset - 1), '\\' . $suiteClassName) === 0) { if (stripos(substr($loadedClass, $offset - 1), '\\' . $suiteClassName) === 0) {
$suiteClassName = $loadedClass; $suiteClassName = $loadedClass;
@ -89,18 +119,16 @@ final class TestSuiteLoader
} }
if (!class_exists($suiteClassName, false)) { if (!class_exists($suiteClassName, false)) {
return new ReflectionClass(WarningTestCase::class); return $this->exceptionFor($suiteClassName, $suiteClassFile);
} }
try { try {
$class = new ReflectionClass($suiteClassName); $class = new ReflectionClass($suiteClassName);
// @codeCoverageIgnoreStart
} catch (ReflectionException $e) { } catch (ReflectionException $e) {
throw new Exception( throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
$e->getMessage(),
(int) $e->getCode(),
$e
);
} }
// @codeCoverageIgnoreEnd
if ($class->isSubclassOf(TestCase::class) && !$class->isAbstract()) { if ($class->isSubclassOf(TestCase::class) && !$class->isAbstract()) {
return $class; return $class;
@ -109,19 +137,39 @@ final class TestSuiteLoader
if ($class->hasMethod('suite')) { if ($class->hasMethod('suite')) {
try { try {
$method = $class->getMethod('suite'); $method = $class->getMethod('suite');
// @codeCoverageIgnoreStart
} catch (ReflectionException $e) { } catch (ReflectionException $e) {
throw new Exception( throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
$e->getMessage(),
(int) $e->getCode(),
$e
);
} }
// @codeCoverageIgnoreEnd
if (!$method->isAbstract() && $method->isPublic() && $method->isStatic()) { if (!$method->isAbstract() && $method->isPublic() && $method->isStatic()) {
return $class; return $class;
} }
} }
return new ReflectionClass(WarningTestCase::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,29 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Bootstrappers;
use Pest\Emitters\DispatchingEmitter;
use PHPUnit\Event;
use ReflectionClass;
/**
* @internal
*/
final class BootEmitter
{
/**
* Boots the Event Emitter.
*/
public function __invoke(): void
{
if (!($baseEmitter = Event\Facade::emitter()) instanceof DispatchingEmitter) {
$reflectedClass = new ReflectionClass(Event\Facade::class);
$reflectedClass->setStaticPropertyValue('emitter', new DispatchingEmitter(
$baseEmitter,
));
}
}
}

View File

@ -16,6 +16,8 @@ final class BootExceptionHandler
*/ */
public function __invoke(): void public function __invoke(): void
{ {
(new Collision\Provider())->register(); $handler = new Collision\Provider();
$handler->register();
} }
} }

View File

@ -36,7 +36,6 @@ final class BootFiles
public function __invoke(): void public function __invoke(): void
{ {
$rootPath = TestSuite::getInstance()->rootPath; $rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory(); $testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
foreach (self::STRUCTURE as $filename) { foreach (self::STRUCTURE as $filename) {

View File

@ -5,11 +5,9 @@ declare(strict_types=1);
namespace Pest\Concerns; namespace Pest\Concerns;
use Closure; use Closure;
use Pest\Support\Backtrace;
use Pest\Support\ChainableClosure; use Pest\Support\ChainableClosure;
use Pest\Support\ExceptionTrace; use Pest\Support\ExceptionTrace;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\ExecutionOrderDependency;
use Throwable; use Throwable;
/** /**
@ -17,11 +15,6 @@ use Throwable;
*/ */
trait Testable trait Testable
{ {
/**
* The Test Case description.
*/
private string $__description;
/** /**
* The Test Case "test" closure. * The Test Case "test" closure.
*/ */
@ -48,46 +41,22 @@ trait Testable
private static ?Closure $__afterAll = null; private static ?Closure $__afterAll = null;
/** /**
* Creates a new Test Case instance. * Resets the test case static properties.
*/ */
public function __construct(Closure $test, string $description, array $data) public static function flush(): void
{ {
$this->__test = $test;
$this->__description = $description;
self::$__beforeAll = null; self::$__beforeAll = null;
self::$__afterAll = null; self::$__afterAll = null;
parent::__construct('__test');
$this->setData($description, $data);
} }
/** /**
* Adds groups to the 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->groups(), $groups)); parent::__construct($name);
$this->setGroups($groups); $this->__test = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($name)->getClosure($this);
}
/**
* Adds dependencies to the Test Case.
*/
public function addDependencies(array $tests): void
{
$className = $this::class;
$tests = array_map(static function (string $test) use ($className): ExecutionOrderDependency {
if (!str_contains($test, '::')) {
$test = "{$className}::{$test}";
}
return new ExecutionOrderDependency($test, '__test');
}, $tests);
$this->setDependencies($tests);
} }
/** /**
@ -148,16 +117,6 @@ trait Testable
: $hook; : $hook;
} }
/**
* Gets the Test Case name.
*/
public function getName(bool $withDataSet = true): string
{
return (str_ends_with(Backtrace::file(), 'TestRunner.php') || Backtrace::line() === 277)
? '__test'
: $this->__description;
}
/** /**
* Gets the Test Case filename. * Gets the Test Case filename.
*/ */
@ -234,26 +193,14 @@ trait Testable
TestSuite::getInstance()->test = null; TestSuite::getInstance()->test = null;
} }
/**
* Gets the Test Case filename and description.
*/
public function toString(): string
{
return \sprintf(
'%s::%s',
self::$__filename,
$this->__description
);
}
/** /**
* Executes the Test Case current test. * Executes the Test Case current test.
* *
* @throws Throwable * @throws Throwable
*/ */
public function __test(): mixed private function __runTest(Closure $closure, ...$args): mixed
{ {
return $this->__callClosure($this->__test, $this->__resolveTestArguments(func_get_args())); return $this->__callClosure($closure, $this->__resolveTestArguments($args));
} }
/** /**

View File

@ -22,6 +22,13 @@ final class Datasets
*/ */
private static array $datasets = []; private static array $datasets = [];
/**
* Holds the withs.
*
* @var array<string, \Closure|iterable|string>
*/
private static array $withs = [];
/** /**
* Sets the given. * Sets the given.
* *
@ -37,34 +44,42 @@ final class Datasets
} }
/** /**
* @return Closure|iterable<int|string, mixed> * Sets the given.
*
* @param Closure|iterable<int|string, mixed> $data
*/ */
public static function get(string $name): Closure|iterable public static function with(string $filename, string $description, Closure|iterable|string $with): void
{ {
if (!array_key_exists($name, self::$datasets)) { self::$withs[$filename . '>>>' . $description] = $with;
throw new DatasetDoesNotExist($name);
} }
return self::$datasets[$name]; /**
* @return Closure|iterable<int|string, mixed>
*/
public static function get(string $filename, string $description): Closure|iterable
{
$dataset = self::$withs[$filename . '>>>' . $description];
return self::resolve($description, $dataset);
} }
/** /**
* Resolves the current dataset to an array value. * Resolves the current dataset to an array value.
* *
* @param array<Closure|iterable<int|string, mixed>|string> $datasets * @param array<Closure|iterable<int|string, mixed>|string> $dataset
* *
* @return array<string, mixed> * @return array<string, mixed>|null
*/ */
public static function resolve(string $description, array $datasets): array public static function resolve(string $description, array $dataset): array|null
{ {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
if (empty($datasets)) { if (empty($dataset)) {
return [$description => []]; return null;
} }
$datasets = self::processDatasets($datasets); $dataset = self::processDatasets($dataset);
$datasetCombinations = self::getDataSetsCombinations($datasets); $datasetCombinations = self::getDataSetsCombinations($dataset);
$dataSetDescriptions = []; $dataSetDescriptions = [];
$dataSetValues = []; $dataSetValues = [];
@ -114,7 +129,11 @@ final class Datasets
$processedDataset = []; $processedDataset = [];
if (is_string($data)) { if (is_string($data)) {
$datasets[$index] = self::get($data); if (!isset(self::$datasets[$data])) {
throw new DatasetDoesNotExist($data);
}
$datasets[$index] = self::$datasets[$data];
} }
if (is_callable($datasets[$index])) { if (is_callable($datasets[$index])) {

View File

@ -1,251 +0,0 @@
<?php
declare(strict_types=1);
namespace Pest\Emitters;
use Pest\Subscribers\EnsureTestsAreLoaded;
use PHPUnit\Event\Code;
use PHPUnit\Event\Code\Throwable;
use PHPUnit\Event\Emitter;
use PHPUnit\Framework\Constraint;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\TextUI\Configuration\Configuration;
use SebastianBergmann\GlobalState\Snapshot;
/**
* @internal
*/
final class DispatchingEmitter implements Emitter
{
/**
* Creates a new Emitter instance.
*/
public function __construct(private Emitter $baseEmitter)
{
// ..
}
public function eventFacadeSealed(): void
{
$this->baseEmitter->eventFacadeSealed(...func_get_args());
}
public function testRunnerStarted(): void
{
$this->baseEmitter->testRunnerStarted(...func_get_args());
}
public function testRunnerConfigured(Configuration $configuration): void
{
$this->baseEmitter->testRunnerConfigured($configuration);
}
public function testRunnerFinished(): void
{
$this->baseEmitter->testRunnerFinished(...func_get_args());
}
public function assertionMade(mixed $value, Constraint\Constraint $constraint, string $message, bool $hasFailed): void
{
$this->baseEmitter->assertionMade($value, $constraint, $message, $hasFailed);
}
public function bootstrapFinished(string $filename): void
{
$this->baseEmitter->bootstrapFinished($filename);
}
public function comparatorRegistered(string $className): void
{
$this->baseEmitter->comparatorRegistered($className);
}
public function extensionLoaded(string $name, string $version): void
{
$this->baseEmitter->extensionLoaded($name, $version);
}
public function globalStateCaptured(Snapshot $snapshot): void
{
$this->baseEmitter->globalStateCaptured($snapshot);
}
public function globalStateModified(Snapshot $snapshotBefore, Snapshot $snapshotAfter, string $diff): void
{
$this->baseEmitter->globalStateModified($snapshotBefore, $snapshotAfter, $diff);
}
public function globalStateRestored(Snapshot $snapshot): void
{
$this->baseEmitter->globalStateRestored($snapshot);
}
public function testErrored(Code\Test $test, Throwable $throwable): void
{
$this->baseEmitter->testErrored(...func_get_args());
}
public function testFailed(Code\Test $test, Throwable $throwable): void
{
$this->baseEmitter->testFailed(...func_get_args());
}
public function testFinished(Code\Test $test): void
{
$this->baseEmitter->testFinished(...func_get_args());
}
public function testOutputPrinted(Code\Test $test, string $output): void
{
$this->baseEmitter->testOutputPrinted(...func_get_args());
}
public function testPassed(Code\Test $test): void
{
$this->baseEmitter->testPassed(...func_get_args());
}
public function testPassedWithWarning(Code\Test $test, Throwable $throwable): void
{
$this->baseEmitter->testPassedWithWarning(...func_get_args());
}
public function testConsideredRisky(Code\Test $test, Throwable $throwable): void
{
$this->baseEmitter->testConsideredRisky(...func_get_args());
}
public function testAborted(Code\Test $test, Throwable $throwable): void
{
$this->baseEmitter->testAborted(...func_get_args());
}
public function testSkipped(Code\Test $test, string $message): void
{
$this->baseEmitter->testSkipped(...func_get_args());
}
public function testPrepared(Code\Test $test): void
{
$this->baseEmitter->testPrepared(...func_get_args());
}
public function testAfterTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void
{
$this->baseEmitter->testAfterTestMethodFinished(...func_get_args());
}
public function testAfterLastTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void
{
$this->baseEmitter->testAfterLastTestMethodFinished(...func_get_args());
}
public function testBeforeFirstTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void
{
$this->baseEmitter->testBeforeFirstTestMethodCalled(...func_get_args());
}
public function testBeforeFirstTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void
{
$this->baseEmitter->testBeforeFirstTestMethodFinished(...func_get_args());
}
public function testBeforeTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void
{
$this->baseEmitter->testBeforeTestMethodCalled(...func_get_args());
}
public function testBeforeTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void
{
$this->baseEmitter->testBeforeTestMethodFinished(...func_get_args());
}
public function testPreConditionCalled(string $testClassName, Code\ClassMethod $calledMethod): void
{
$this->baseEmitter->testPreConditionCalled(...func_get_args());
}
public function testPreConditionFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void
{
$this->baseEmitter->testPreConditionFinished(...func_get_args());
}
public function testPostConditionCalled(string $testClassName, Code\ClassMethod $calledMethod): void
{
$this->baseEmitter->testPostConditionCalled(...func_get_args());
}
public function testPostConditionFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void
{
$this->baseEmitter->testPostConditionFinished(...func_get_args());
}
public function testAfterTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void
{
$this->baseEmitter->testAfterTestMethodCalled(...func_get_args());
}
public function testAfterLastTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void
{
$this->baseEmitter->testAfterLastTestMethodCalled(...func_get_args());
}
public function testMockObjectCreated(string $className): void
{
$this->baseEmitter->testMockObjectCreated(...func_get_args());
}
public function testMockObjectCreatedForTrait(string $traitName): void
{
$this->baseEmitter->testMockObjectCreatedForTrait(...func_get_args());
}
public function testMockObjectCreatedForAbstractClass(string $className): void
{
$this->baseEmitter->testMockObjectCreatedForAbstractClass(...func_get_args());
}
public function testMockObjectCreatedFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void
{
$this->baseEmitter->testMockObjectCreatedFromWsdl(...func_get_args());
}
public function testPartialMockObjectCreated(string $className, string ...$methodNames): void
{
$this->baseEmitter->testPartialMockObjectCreated(...func_get_args());
}
public function testTestProxyCreated(string $className, array $constructorArguments): void
{
$this->baseEmitter->testTestProxyCreated(...func_get_args());
}
public function testTestStubCreated(string $className): void
{
$this->baseEmitter->testTestStubCreated(...func_get_args());
}
public function testSuiteLoaded(TestSuite $testSuite): void
{
EnsureTestsAreLoaded::setTestSuite($testSuite);
$this->baseEmitter->testSuiteLoaded(...func_get_args());
}
public function testSuiteSorted(int $executionOrder, int $executionOrderDefects, bool $resolveDependencies): void
{
$this->baseEmitter->testSuiteSorted(...func_get_args());
}
public function testSuiteStarted(TestSuite $testSuite): void
{
$this->baseEmitter->testSuiteStarted(...func_get_args());
}
public function testSuiteFinished(TestSuite $testSuite, TestResult $result): void
{
$this->baseEmitter->testSuiteFinished(...func_get_args());
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Str;
/**
* @internal
*/
final class Depends
{
/**
* Adds annotations regarding the "depends" feature.
*/
public function add(TestCaseMethodFactory $method, array $annotations): array
{
foreach ($method->depends as $depend) {
$depend = Str::evaluable($depend);
$annotations[] = "@depends $depend";
}
return $annotations;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Annotations;
use Pest\Factories\TestCaseMethodFactory;
/**
* @internal
*/
final class Groups
{
/**
* Adds annotations regarding the "groups" feature.
*/
public function add(TestCaseMethodFactory $method, array $annotations): array
{
foreach ($method->groups as $group) {
$annotations[] = "@group $group";
}
return $annotations;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Pest\Factories\Concerns;
use Pest\Support\HigherOrderMessageCollection;
trait HigherOrderable
{
/**
* The higher order messages that are chainable.
*/
public HigherOrderMessageCollection $chains;
/**
* The higher order messages that are "factory" proxyable.
*/
public HigherOrderMessageCollection $factoryProxies;
/**
* The higher order messages that are proxyable.
*/
public HigherOrderMessageCollection $proxies;
/**
* Boot the higher order properties.
*/
private function bootHigherOrderable(): void
{
$this->chains = new HigherOrderMessageCollection();
$this->factoryProxies = new HigherOrderMessageCollection();
$this->proxies = new HigherOrderMessageCollection();
}
}

View File

@ -4,16 +4,18 @@ declare(strict_types=1);
namespace Pest\Factories; namespace Pest\Factories;
use Closure;
use ParseError; use ParseError;
use Pest\Concerns; use Pest\Concerns;
use Pest\Contracts\HasPrintableTestCaseName; use Pest\Contracts\HasPrintableTestCaseName;
use Pest\Datasets; use Pest\Datasets;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\ShouldNotHappen;
use Pest\Support\HigherOrderMessageCollection; use Pest\Exceptions\TestAlreadyExist;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\Plugins\Environment;
use Pest\Support\Reflection;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RuntimeException; use RuntimeException;
@ -22,22 +24,17 @@ use RuntimeException;
*/ */
final class TestCaseFactory final class TestCaseFactory
{ {
/** use HigherOrderable;
* Determines if the Test Case will be the "only" being run.
*/
public bool $only = false;
/** /**
* The Test Case closure. * The list of annotations.
*/
public Closure $test;
/**
* The Test Case Dataset, if any.
* *
* @var array<Closure|iterable<int|string, mixed>|string> * @var array<int, class-string>
*/ */
public array $datasets = []; private static array $annotations = [
Annotations\Depends::class,
Annotations\Groups::class,
];
/** /**
* The FQN of the Test Case class. * The FQN of the Test Case class.
@ -47,7 +44,14 @@ final class TestCaseFactory
public string $class = TestCase::class; public string $class = TestCase::class;
/** /**
* An array of FQN of the Test Case traits. * The list of class methods.
*
* @var array<string, TestCaseMethodFactory>
*/
public array $methods = [];
/**
* The list of class traits.
* *
* @var array <int, class-string> * @var array <int, class-string>
*/ */
@ -56,81 +60,48 @@ final class TestCaseFactory
Concerns\Expectable::class, Concerns\Expectable::class,
]; ];
/**
* The higher order messages for the factory that are proxyable.
*/
public HigherOrderMessageCollection $factoryProxies;
/**
* The higher order messages that are proxyable.
*/
public HigherOrderMessageCollection $proxies;
/**
* The higher order messages that are chainable.
*/
public HigherOrderMessageCollection $chains;
/** /**
* Creates a new Factory instance. * Creates a new Factory instance.
*/ */
public function __construct( public function __construct(
public string $filename, public string $filename
public ?string $description, ) {
Closure $closure = null) $this->bootHigherOrderable();
{ }
$this->test = $closure ?? fn () => Assert::getCount() > 0 ?: self::markTestIncomplete();
$this->factoryProxies = new HigherOrderMessageCollection(); public function make(): void
$this->proxies = new HigherOrderMessageCollection(); {
$this->chains = new HigherOrderMessageCollection(); $methods = array_filter($this->methods, function ($method) {
return count($onlyTestCases = $this->methodsUsingOnly()) === 0 || in_array($method, $onlyTestCases, true);
});
if (count($this->methods) > 0) {
$this->evaluate($this->filename, $methods);
}
} }
/** /**
* Makes the Test Case classes. * Returns all the "only" methods.
* *
* @return array<int, TestCase> * @return array<int, TestCaseMethodFactory>
*/ */
public function make(): array public function methodsUsingOnly(): array
{ {
if ($this->description === null) { if (Environment::name() === Environment::CI) {
throw ShouldNotHappen::fromMessage('Description can not be empty.'); return [];
} }
$chains = $this->chains; return array_filter($this->methods, static fn ($method): bool => $method->only);
$proxies = $this->proxies;
$factoryTest = $this->test;
$testClosure = function () use ($chains, $proxies, $factoryTest): mixed {
$proxies->proxy($this);
$chains->chain($this);
/* @phpstan-ignore-next-line */
return call_user_func(Closure::bind($factoryTest, $this, $this::class), ...func_get_args());
};
$className = $this->makeClassFromFilename($this->filename);
$createTest = function ($description, $data) use ($className, $testClosure) {
$testCase = new $className($testClosure, $description, $data);
$this->factoryProxies->proxy($testCase);
return $testCase;
};
$datasets = Datasets::resolve($this->description, $this->datasets);
return array_map($createTest, array_keys($datasets), $datasets);
} }
/** /**
* Makes a Fully Qualified Class Name from the given filename. * Creates a Test Case class using a runtime evaluate.
*/ */
public function makeClassFromFilename(string $filename): string public function evaluate(string $filename, array $methods): string
{ {
if ('\\' === DIRECTORY_SEPARATOR) { if ('\\' === DIRECTORY_SEPARATOR) {
// In case Windows, strtolower drive name, like in UsesCall. // In case Windows, strtolower drive name, like in UsesCall.
$filename = (string) preg_replace_callback('~^(?P<drive>[a-z]+:\\\)~i', fn ($match): string => 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))); $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename)));
@ -152,7 +123,9 @@ final class TestCaseFactory
} }
$hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class);
$traitsCode = sprintf('use %s;', implode(', ', array_map(fn ($trait): string => sprintf('\%s', $trait), $this->traits))); $traitsCode = sprintf('use %s;', implode(', ', array_map(
static fn ($trait): string => sprintf('\%s', $trait), $this->traits))
);
$partsFQN = explode('\\', $classFQN); $partsFQN = explode('\\', $classFQN);
$className = array_pop($partsFQN); $className = array_pop($partsFQN);
@ -164,14 +137,65 @@ final class TestCaseFactory
$classFQN .= $className; $classFQN .= $className;
} }
$methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string {
$methodName = Str::evaluable($method->description);
$datasetsCode = '';
$annotations = ['@test'];
foreach (self::$annotations as $annotation) {
$annotations = (new $annotation())->add($method, $annotations);
}
if (!empty($method->datasets)) {
$dataProviderName = $methodName . '_dataset';
$annotations[] = "@dataProvider $dataProviderName";
Datasets::with($method->filename, $methodName, $method->datasets);
$datasetsCode = <<<EOF
public function $dataProviderName()
{
return __PestDatasets::get(self::\$__filename, "$methodName");
}
EOF;
}
$annotations = implode('', array_map(
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
));
return <<<EOF
/**$annotations
*/
public function $methodName()
{
return \$this->__runTest(
\$this->__test,
...func_get_args(),
);
}
$datasetsCode
EOF;
}, $methods));
try { try {
eval(" eval("
namespace $namespace; namespace $namespace;
use Pest\Datasets as __PestDatasets;
use Pest\TestSuite as __PestTestSuite;
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
$traitsCode $traitsCode
private static \$__filename = '$filename'; private static \$__filename = '$filename';
$methodsCode
} }
"); ");
} catch (ParseError $caught) { } catch (ParseError $caught) {
@ -182,11 +206,40 @@ final class TestCaseFactory
} }
/** /**
* Determine if the test case will receive argument input from Pest, or not. * Adds the given Method to the Test Case.
*/ */
public function __receivesArguments(): bool public function addMethod(TestCaseMethodFactory $method): void
{ {
return count($this->datasets) > 0 if ($method->description === null) {
|| $this->factoryProxies->count('addDependencies') > 0; throw ShouldNotHappen::fromMessage('The test description may not be empty.');
}
if (isset($this->methods[$method->description])) {
throw new TestAlreadyExist($method->filename, $method->description);
}
if (!$method->receivesArguments()) {
$arguments = Reflection::getFunctionArguments($method->closure);
if (count($arguments) > 0) {
throw new DatasetMissing($method->filename, $method->description, $arguments);
}
}
$this->methods[$method->description] = $method;
}
/**
* Gets a Method by the given name.
*/
public function getMethod(string $methodName): TestCaseMethodFactory
{
foreach ($this->methods as $method) {
if (Str::evaluable($method->description) === $methodName) {
return $method;
}
}
throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName));
} }
} }

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Pest\Factories;
use Closure;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Factories\Concerns\HigherOrderable;
use Pest\TestSuite;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
final class TestCaseMethodFactory
{
use HigherOrderable;
/**
* Determines if the Test Case will be the "only" being run.
*/
public bool $only = false;
/**
* The Test Case Dataset, if any.
*
* @var array<Closure|iterable<int|string, mixed>|string>
*/
public array $datasets = [];
/**
* The Test Case depends, if any.
*
* @var array<int, string>
*/
public array $depends = [];
/**
* The Test Case groups, if any.
*
* @var array<int, string>
*/
public array $groups = [];
/**
* Creates a new Factory instance.
*/
public function __construct(
public string $filename,
public ?string $description,
public ?Closure $closure,
) {
if ($this->closure === null) {
$this->closure = function () {
Assert::getCount() > 0 ?: self::markTestIncomplete();
};
}
$this->bootHigherOrderable();
}
/**
* Makes the Test Case classes.
*/
public function getClosure(TestCase $concrete): Closure
{
$concrete::flush();
if ($this->description === null) {
throw ShouldNotHappen::fromMessage('Description can not be empty.');
}
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename);
$testCase->factoryProxies->proxy($concrete);
$this->factoryProxies->proxy($concrete);
$method = $this;
return function () use ($testCase, $method, $closure): mixed {
$testCase->proxies->proxy($this);
$method->proxies->proxy($this);
$testCase->chains->chain($this);
$method->chains->chain($this);
return call_user_func(Closure::bind($closure, $this, $this::class), ...func_get_args());
};
}
/**
* Determine if the test case will receive argument input from Pest, or not.
*/
public function receivesArguments(): bool
{
return count($this->datasets) > 0 || count($this->depends) > 0;
}
}

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
{
// ..
}

View File

@ -18,7 +18,6 @@ final class Kernel
*/ */
private static array $bootstrappers = [ private static array $bootstrappers = [
Bootstrappers\BootExceptionHandler::class, Bootstrappers\BootExceptionHandler::class,
Bootstrappers\BootEmitter::class,
Bootstrappers\BootSubscribers::class, Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class, Bootstrappers\BootFiles::class,
]; ];

View File

@ -43,7 +43,7 @@ final class TeamCity extends DefaultResultPrinter
/** /**
* Creates a new printer instance. * Creates a new printer instance.
*/ */
public function __construct(resource|string|null $out, bool $verbose, string $colors) public function __construct(string|null $out, bool $verbose, string $colors)
{ {
parent::__construct($out, $verbose, $colors); parent::__construct($out, $verbose, $colors);
$this->phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors); $this->phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors);

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls; namespace Pest\PendingCalls;
use Closure; use Closure;
use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\HigherOrderCallables; use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure; use Pest\Support\NullClosure;
@ -22,7 +22,7 @@ final class TestCall
/** /**
* The Test Case Factory. * The Test Case Factory.
*/ */
private TestCaseFactory $testCaseFactory; private TestCaseMethodFactory $testCaseMethod;
/** /**
* If test call is descriptionLess. * If test call is descriptionLess.
@ -38,7 +38,7 @@ final class TestCall
string $description = null, string $description = null,
Closure $closure = null Closure $closure = null
) { ) {
$this->testCaseFactory = new TestCaseFactory($filename, $description, $closure); $this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
$this->descriptionLess = $description === null; $this->descriptionLess = $description === null;
} }
@ -48,7 +48,7 @@ final class TestCall
public function throws(string $exception, string $exceptionMessage = null): TestCall public function throws(string $exception, string $exceptionMessage = null): TestCall
{ {
if (class_exists($exception)) { if (class_exists($exception)) {
$this->testCaseFactory $this->testCaseMethod
->proxies ->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]); ->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]);
} else { } else {
@ -56,7 +56,7 @@ final class TestCall
} }
if (is_string($exceptionMessage)) { if (is_string($exceptionMessage)) {
$this->testCaseFactory $this->testCaseMethod
->proxies ->proxies
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]); ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]);
} }
@ -90,10 +90,10 @@ final class TestCall
* *
* @param array<\Closure|iterable<int|string, mixed>|string> $data * @param array<\Closure|iterable<int|string, mixed>|string> $data
*/ */
public function with(...$data): TestCall public function with(Closure|iterable|string ...$data): TestCall
{ {
foreach ($data as $dataset) { foreach ($data as $dataset) {
$this->testCaseFactory->datasets[] = $dataset; $this->testCaseMethod->datasets[] = $dataset;
} }
return $this; return $this;
@ -102,11 +102,11 @@ final class TestCall
/** /**
* Sets the test depends. * Sets the test depends.
*/ */
public function depends(string ...$tests): TestCall public function depends(string ...$depends): TestCall
{ {
$this->testCaseFactory foreach ($depends as $depend) {
->factoryProxies $this->testCaseMethod->depends[] = $depend;
->add(Backtrace::file(), Backtrace::line(), 'addDependencies', [$tests]); }
return $this; return $this;
} }
@ -116,7 +116,7 @@ final class TestCall
*/ */
public function only(): TestCall public function only(): TestCall
{ {
$this->testCaseFactory->only = true; $this->testCaseMethod->only = true;
return $this; return $this;
} }
@ -126,9 +126,9 @@ final class TestCall
*/ */
public function group(string ...$groups): TestCall public function group(string ...$groups): TestCall
{ {
$this->testCaseFactory foreach ($groups as $group) {
->factoryProxies $this->testCaseMethod->groups[] = $group;
->add(Backtrace::file(), Backtrace::line(), 'addGroups', [$groups]); }
return $this; return $this;
} }
@ -153,7 +153,7 @@ final class TestCall
/** @var callable(): bool $condition */ /** @var callable(): bool $condition */
$condition = $condition->bindTo(null); $condition = $condition->bindTo(null);
$this->testCaseFactory $this->testCaseMethod
->chains ->chains
->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]); ->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]);
@ -185,16 +185,16 @@ final class TestCall
*/ */
private function addChain(string $name, array $arguments = null): self private function addChain(string $name, array $arguments = null): self
{ {
$this->testCaseFactory $this->testCaseMethod
->chains ->chains
->add(Backtrace::file(), Backtrace::line(), $name, $arguments); ->add(Backtrace::file(), Backtrace::line(), $name, $arguments);
if ($this->descriptionLess) { if ($this->descriptionLess) {
$exporter = new Exporter(); $exporter = new Exporter();
if ($this->testCaseFactory->description !== null) { if ($this->testCaseMethod->description !== null) {
$this->testCaseFactory->description .= ' → '; $this->testCaseMethod->description .= ' → ';
} }
$this->testCaseFactory->description .= $arguments === null $this->testCaseMethod->description .= $arguments === null
? $name ? $name
: sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments)); : sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments));
} }
@ -207,6 +207,6 @@ final class TestCall
*/ */
public function __destruct() public function __destruct()
{ {
$this->testSuite->tests->set($this->testCaseFactory); $this->testSuite->tests->set($this->testCaseMethod);
} }
} }

View File

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

View File

@ -5,16 +5,11 @@ declare(strict_types=1);
namespace Pest\Repositories; namespace Pest\Repositories;
use Closure; use Closure;
use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Exceptions\TestAlreadyExist;
use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseAlreadyInUse;
use Pest\Exceptions\TestCaseClassOrTraitNotFound; use Pest\Exceptions\TestCaseClassOrTraitNotFound;
use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseFactory;
use Pest\Plugins\Environment; use Pest\Factories\TestCaseMethodFactory;
use Pest\Support\Reflection;
use Pest\Support\Str; use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
/** /**
@ -22,15 +17,10 @@ use PHPUnit\Framework\TestCase;
*/ */
final class TestRepository final class TestRepository
{ {
/**
* @var non-empty-string
*/
private const SEPARATOR = '>>>';
/** /**
* @var array<string, TestCaseFactory> * @var array<string, TestCaseFactory>
*/ */
private array $state = []; private array $testCases = [];
/** /**
* @var array<string, array<int, array<int, string|Closure>>> * @var array<string, array<int, array<int, string|Closure>>>
@ -42,7 +32,7 @@ final class TestRepository
*/ */
public function count(): int public function count(): int
{ {
return count($this->state); return count($this->testCases);
} }
/** /**
@ -52,74 +42,13 @@ final class TestRepository
*/ */
public function getFilenames(): array public function getFilenames(): array
{ {
$testsWithOnly = $this->testsUsingOnly(); $testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase) => count($testCase->methodsUsingOnly()) > 0);
return array_values(array_map(fn (TestCaseFactory $factory): string => $factory->filename, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state)); if (count($testCases) === 0) {
$testCases = $this->testCases;
} }
/** return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $testCases));
* Calls the given callable foreach test case.
*/
public function build(TestSuite $testSuite, callable $each): void
{
$startsWith = fn (string $target, string $directory): bool => 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;
}
}
$testCase->factoryProxies->add($filename, 0, 'addGroups', [$groups]);
$testCase->factoryProxies->add($filename, 0, '__addBeforeAll', [$hooks[0] ?? null]);
$testCase->factoryProxies->add($filename, 0, '__addBeforeEach', [$hooks[1] ?? null]);
$testCase->factoryProxies->add($filename, 0, '__addAfterEach', [$hooks[2] ?? null]);
$testCase->factoryProxies->add($filename, 0, '__addAfterAll', [$hooks[3] ?? null]);
}
};
foreach ($this->state as $key => $test) {
$setClassName($test, $key);
}
}
$onlyState = $this->testsUsingOnly();
$state = count($onlyState) > 0 ? $onlyState : $this->state;
foreach ($state as $testFactory) {
/** @var TestCaseFactory $testFactory */
$tests = $testFactory->make($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, fn ($testFactory): bool => $testFactory->only);
} }
/** /**
@ -151,27 +80,73 @@ final class TestRepository
} }
} }
/** public function get($filename): TestCaseFactory
* Sets a test case by the given filename and description.
*/
public function set(TestCaseFactory $test): void
{ {
if ($test->description === null) { return $this->testCases[$filename];
throw ShouldNotHappen::fromMessage('Trying to create a test without description.');
} }
if (array_key_exists(sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description), $this->state)) { /**
throw new TestAlreadyExist($test->filename, $test->description); * Sets a new test case method.
*/
public function set(TestCaseMethodFactory $method): void
{
if (!isset($this->testCases[$method->filename])) {
$this->testCases[$method->filename] = new TestCaseFactory($method->filename);
} }
if (!$test->__receivesArguments()) { $this->testCases[$method->filename]->addMethod($method);
$arguments = Reflection::getFunctionArguments($test->test); }
if (count($arguments) > 0) { /**
throw new DatasetMissing($test->filename, $test->description, $arguments); * Makes a Test Case from the given filename, if exists.
*/
public function makeIfExists(string $filename): void
{
if (isset($this->testCases[$filename])) {
$this->make($this->testCases[$filename]);
} }
} }
$this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test; /**
* Makes a Test Case using the given factory.
*/
private function make(TestCaseFactory $testCase): void
{
$startsWith = static fn (string $target, string $directory): bool => Str::startsWith($target, $directory . DIRECTORY_SEPARATOR);
foreach ($this->uses as $path => $uses) {
[$classOrTraits, $groups, $hooks] = $uses;
if ((!is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) {
foreach ($classOrTraits as $class) {
/** @var string $class */
if (class_exists($class)) {
if ($testCase->class !== TestCase::class) {
throw new TestCaseAlreadyInUse($testCase->class, $class, $testCase->filename);
}
$testCase->class = $class;
} elseif (trait_exists($class)) {
$testCase->traits[] = $class;
}
}
foreach ($testCase->methods as $method) {
foreach ($groups as $group) {
$method->groups[] = $group;
}
}
foreach ($testCase->methods as $method) {
$method->groups = array_merge($groups, $method->groups);
}
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeAll', [$hooks[0] ?? null]);
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeEach', [$hooks[1] ?? null]);
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterEach', [$hooks[2] ?? null]);
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterAll', [$hooks[3] ?? null]);
}
}
$testCase->make();
} }
} }

View File

@ -25,6 +25,8 @@ final class EnsureTestsAreLoaded implements LoadedSubscriber
*/ */
public function notify(Loaded $event): void public function notify(Loaded $event): void
{ {
/*
$this->removeWarnings(self::$testSuite); $this->removeWarnings(self::$testSuite);
$testSuites = []; $testSuites = [];
@ -47,6 +49,7 @@ final class EnsureTestsAreLoaded implements LoadedSubscriber
} }
self::$testSuite->addTestSuite($testTestSuite); self::$testSuite->addTestSuite($testTestSuite);
} }
*/
} }
/** /**

View File

@ -54,8 +54,7 @@ final class HigherOrderMessage
try { try {
return is_array($this->arguments) return is_array($this->arguments)
? Reflection::call($target, $this->name, $this->arguments) ? Reflection::call($target, $this->name, $this->arguments)
: $target->{$this->name}; : $target->{$this->name}; /* @phpstan-ignore-line */
/* @phpstan-ignore-line */
} catch (Throwable $throwable) { } catch (Throwable $throwable) {
Reflection::setPropertyValue($throwable, 'file', $this->filename); Reflection::setPropertyValue($throwable, 'file', $this->filename);
Reflection::setPropertyValue($throwable, 'line', $this->line); Reflection::setPropertyValue($throwable, 'line', $this->line);

View File

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

View File

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

View File

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

View File

@ -2,53 +2,55 @@
use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\DatasetMissing;
use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestAlreadyExist;
use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseMethodFactory;
use Pest\Plugins\Environment; use Pest\Plugins\Environment;
use Pest\TestSuite; use Pest\TestSuite;
it('does not allow to add the same test description twice', function () { it('does not allow to add the same test description twice', function () {
$testSuite = new TestSuite(getcwd(), 'tests'); $testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {}; $method = new TestCaseMethodFactory('foo', 'bar', null);
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test));
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); $testSuite->tests->set($method);
$testSuite->tests->set($method);
})->throws( })->throws(
TestAlreadyExist::class, TestAlreadyExist::class,
sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__), sprintf('A test with the description `%s` already exist in the filename `%s`.', 'bar', 'foo'),
); );
it('alerts users about tests with arguments but no input', function () { it('alerts users about tests with arguments but no input', function () {
$testSuite = new TestSuite(getcwd(), 'tests'); $testSuite = new TestSuite(getcwd(), 'tests');
$test = function (int $arg) {};
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); $method = new TestCaseMethodFactory('foo', 'bar', function (int $arg) {});
$testSuite->tests->set($method);
})->throws( })->throws(
DatasetMissing::class, DatasetMissing::class,
sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__), sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'bar', 1, 'int $arg', 'foo'),
); );
it('can return an array of all test suite filenames', function () { it('can return an array of all test suite filenames', function () {
$testSuite = TestSuite::getInstance(getcwd(), 'tests'); $testSuite = TestSuite::getInstance(getcwd(), 'tests');
$test = function () {};
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); $testSuite->tests->set(new TestCaseMethodFactory('a', 'b', null));
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'bar', $test)); $testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null));
expect($testSuite->tests->getFilenames())->toEqual([ expect($testSuite->tests->getFilenames())->toEqual([
__FILE__, 'a',
__FILE__, 'c',
]); ]);
}); });
it('can filter the test suite filenames to those with the only method', function () { it('can filter the test suite filenames to those with the only method', function () {
$testSuite = new TestSuite(getcwd(), 'tests'); $testSuite = new TestSuite(getcwd(), 'tests');
$test = function () {};
$testWithOnly = new TestCaseFactory(__FILE__, 'foo', $test); $testWithOnly = new TestCaseMethodFactory('a', 'b', null);
$testWithOnly->only = true; $testWithOnly->only = true;
$testSuite->tests->set($testWithOnly); $testSuite->tests->set($testWithOnly);
$testSuite->tests->set(new TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test)); $testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null));
expect($testSuite->tests->getFilenames())->toEqual([ expect($testSuite->tests->getFilenames())->toEqual([
__FILE__, 'a',
]); ]);
}); });
@ -59,15 +61,15 @@ it('does not filter the test suite filenames to those with the only method when
$test = function () {}; $test = function () {};
$testWithOnly = new TestCaseFactory(__FILE__, 'foo', $test); $testWithOnly = new TestCaseMethodFactory('a', 'b', null);
$testWithOnly->only = true; $testWithOnly->only = true;
$testSuite->tests->set($testWithOnly); $testSuite->tests->set($testWithOnly);
$testSuite->tests->set(new TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test)); $testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null));
expect($testSuite->tests->getFilenames())->toEqual([ expect($testSuite->tests->getFilenames())->toEqual([
__FILE__, 'a',
'Baz/Bar/Boo.php', 'c',
]); ]);
Environment::name($previousEnvironment); Environment::name($previousEnvironment);