mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
Merge branch 'pipes-and-interceptors' into next-1
# Conflicts: # src/Concerns/Extendable.php # src/CoreExpectation.php # src/Expectation.php # src/Support/ExpectationPipeline.php # src/Support/Extendable.php
This commit is contained in:
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -2,3 +2,4 @@
|
||||
|
||||
github: [nunomaduro,owenvoke,olivernybroe,octoper,lukeraymonddowning]
|
||||
patreon: nunomaduro
|
||||
custom: https://www.paypal.com/paypalme/enunomaduro
|
||||
|
||||
7
.github/workflows/tests.yml
vendored
7
.github/workflows/tests.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-latest] # (macos-latest, windows-latest) 2.x-dev is under development
|
||||
php: ['8.0', '8.1']
|
||||
dependency-version: [prefer-lowest, prefer-stable]
|
||||
parallel: ['', '--parallel']
|
||||
@ -38,8 +38,13 @@ jobs:
|
||||
- name: Install PHP dependencies
|
||||
run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress
|
||||
|
||||
- name: Unit Tests
|
||||
run: php bin/pest --colors=always --exclude-group=integration
|
||||
|
||||
- name: Unit Tests
|
||||
run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }}
|
||||
if: ${{ false }} # 2.x-dev is under development
|
||||
|
||||
- name: Integration Tests
|
||||
run: php bin/pest --colors=always --group=integration
|
||||
if: ${{ false }} # 2.x-dev is under development
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'overrides')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs')
|
||||
->in(__DIR__ . DIRECTORY_SEPARATOR . 'src')
|
||||
->append(['.php-cs-fixer.dist.php']);
|
||||
|
||||
18
README.md
18
README.md
@ -19,14 +19,18 @@
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Pest development. If you are interested in becoming a sponsor, please visit the Nuno Maduro's [Sponsors page](https://github.com/sponsors/nunomaduro).
|
||||
|
||||
### Platinum Sponsors
|
||||
|
||||
- **[Spatie](https://spatie.be)**
|
||||
- **[Worksome](https://www.worksome.com/)**
|
||||
|
||||
### Premium Sponsors
|
||||
|
||||
- **[Akaunting](https://akaunting.com)**
|
||||
- **[Auth0](https://auth0.com)**
|
||||
- **[Codecourse](https://codecourse.com/)**
|
||||
- **[Fathom Analytics](https://usefathom.com/)**
|
||||
- **[Meema](https://meema.io)**
|
||||
- **[Scout APM](https://scoutapm.com)**
|
||||
- **[Spatie](https://spatie.be)**
|
||||
- [Akaunting](https://akaunting.com)
|
||||
- [Auth0](https://auth0.com)
|
||||
- [Codecourse](https://codecourse.com/)
|
||||
- [Fathom Analytics](https://usefathom.com/)
|
||||
- [Meema](https://meema.io)
|
||||
- [Scout APM](https://scoutapm.com)
|
||||
|
||||
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
When releasing a new version of Pest there are some checks and updates that need to be done:
|
||||
|
||||
> **For Pest v1 you should use the `1.x` branch instead.**
|
||||
|
||||
- Clear your local repository with: `git add . && git reset --hard && git checkout master`
|
||||
- On the GitHub repository, check the contents of [github.com/pestphp/pest/compare/{latest_version}...master](https://github.com/pestphp/pest/compare/{latest_version}...master) and update the [changelog](CHANGELOG.md) file with the main changes for this release
|
||||
- Update the version number in [src/Pest.php](src/Pest.php)
|
||||
|
||||
@ -48,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-mock": "^1.0"
|
||||
"pestphp/pest-dev-tools": "dev-master"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
@ -63,17 +62,15 @@
|
||||
"scripts": {
|
||||
"lint": "php-cs-fixer fix -v",
|
||||
"test:lint": "php-cs-fixer fix -v --dry-run",
|
||||
"test:types": "phpstan analyse --ansi --memory-limit=-1",
|
||||
"test:types": "phpstan analyse --ansi --memory-limit=-1 --debug",
|
||||
"test:unit": "php bin/pest --colors=always --exclude-group=integration",
|
||||
"test:parallel": "php bin/pest -p --colors=always --exclude-group=integration",
|
||||
"test:integration": "php bin/pest --colors=always --group=integration",
|
||||
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
|
||||
"test:parallel": "exit 1",
|
||||
"test:integration": "exit 1",
|
||||
"update:snapshots": "exit 1",
|
||||
"test": [
|
||||
"@test:lint",
|
||||
"@test:types",
|
||||
"@test:unit",
|
||||
"@test:parallel",
|
||||
"@test:integration"
|
||||
"@test:unit"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
||||
@ -1,22 +1,5 @@
|
||||
<?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>.
|
||||
* All rights reserved.
|
||||
@ -51,35 +34,82 @@ use PHPUnit\Framework\WarningTestCase;
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$suiteClassName = basename($suiteClassFile, '.php');
|
||||
$loadedClasses = get_declared_classes();
|
||||
$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(), $loadedClasses)
|
||||
array_diff(
|
||||
get_declared_classes(),
|
||||
array_merge(
|
||||
self::$declaredClasses,
|
||||
self::$loadedClasses
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (empty($loadedClasses)) {
|
||||
return new ReflectionClass(WarningTestCase::class);
|
||||
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 ($loadedClasses as $loadedClass) {
|
||||
|
||||
foreach (self::$loadedClasses as $loadedClass) {
|
||||
if (stripos(substr($loadedClass, $offset - 1), '\\' . $suiteClassName) === 0) {
|
||||
$suiteClassName = $loadedClass;
|
||||
|
||||
@ -89,18 +119,16 @@ final class TestSuiteLoader
|
||||
}
|
||||
|
||||
if (!class_exists($suiteClassName, false)) {
|
||||
return new ReflectionClass(WarningTestCase::class);
|
||||
return $this->exceptionFor($suiteClassName, $suiteClassFile);
|
||||
}
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($suiteClassName);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
throw new Exception(
|
||||
$e->getMessage(),
|
||||
(int) $e->getCode(),
|
||||
$e
|
||||
);
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ($class->isSubclassOf(TestCase::class) && !$class->isAbstract()) {
|
||||
return $class;
|
||||
@ -109,19 +137,39 @@ final class TestSuiteLoader
|
||||
if ($class->hasMethod('suite')) {
|
||||
try {
|
||||
$method = $class->getMethod('suite');
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
throw new Exception(
|
||||
$e->getMessage(),
|
||||
(int) $e->getCode(),
|
||||
$e
|
||||
);
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if (!$method->isAbstract() && $method->isPublic() && $method->isStatic()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
13
phpstan.neon
13
phpstan.neon
@ -13,6 +13,7 @@ parameters:
|
||||
reportUnmatchedIgnoredErrors: true
|
||||
|
||||
ignoreErrors:
|
||||
- "#with a nullable type declaration#"
|
||||
- "#type mixed is not subtype of native#"
|
||||
- "#is not allowed to extend#"
|
||||
- "#Language construct eval#"
|
||||
@ -20,15 +21,3 @@ parameters:
|
||||
- "#has parameter \\$closure with default value.#"
|
||||
- "#has parameter \\$description with default value.#"
|
||||
- "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#"
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#invalid typehint type Pest\\Concerns\\Testable#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#is not subtype of native type PHPUnit\\Framework\\Test#'
|
||||
path: src/Logging
|
||||
-
|
||||
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getPrintableTestCaseName\(\)#'
|
||||
path: src/Logging
|
||||
|
||||
@ -1,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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,6 +16,8 @@ final class BootExceptionHandler
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
(new Collision\Provider())->register();
|
||||
$handler = new Collision\Provider();
|
||||
|
||||
$handler->register();
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,6 @@ final class BootFiles
|
||||
public function __invoke(): void
|
||||
{
|
||||
$rootPath = TestSuite::getInstance()->rootPath;
|
||||
|
||||
$testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory();
|
||||
|
||||
foreach (self::STRUCTURE as $filename) {
|
||||
|
||||
@ -15,10 +15,9 @@ final class BootSubscribers
|
||||
/**
|
||||
* The Kernel subscribers.
|
||||
*
|
||||
* @var array<int, class-string>
|
||||
* @var array<int, class-string<\PHPUnit\Event\Subscriber>>
|
||||
*/
|
||||
private static array $subscribers = [
|
||||
Subscribers\EnsureTestsAreLoaded::class,
|
||||
Subscribers\EnsureConfigurationIsValid::class,
|
||||
Subscribers\EnsureConfigurationDefaults::class,
|
||||
];
|
||||
|
||||
@ -6,7 +6,6 @@ namespace Pest\Concerns;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Closure;
|
||||
use Pest\Expectation;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -20,7 +19,7 @@ trait Extendable
|
||||
*/
|
||||
private static array $extends = [];
|
||||
|
||||
/** @var array<string, array<Closure(Closure, mixed ...$arguments): void>> */
|
||||
/** @var array<string, array<Closure(Closure $next, mixed ...$arguments): void>> */
|
||||
private static array $pipes = [];
|
||||
|
||||
/**
|
||||
@ -31,6 +30,38 @@ trait Extendable
|
||||
static::$extends[$name] = $extend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a pipe to be applied before an expectation is checked.
|
||||
*/
|
||||
public static function pipe(string $name, Closure $pipe): void
|
||||
{
|
||||
self::$pipes[$name][] = $pipe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recister an interceptor that should replace an existing expectation.
|
||||
*/
|
||||
public static function intercept(string $name, string|Closure $filter, Closure $handler): void
|
||||
{
|
||||
if (is_string($filter)) {
|
||||
$filter = function ($value) use ($filter): bool {
|
||||
return $value instanceof $filter;
|
||||
};
|
||||
}
|
||||
|
||||
self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
if ($filter($this->value)) {
|
||||
//@phpstan-ignore-next-line
|
||||
$handler->bindTo($this, get_class($this))(...$arguments);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$next();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given extend name is registered.
|
||||
*/
|
||||
@ -40,43 +71,6 @@ trait Extendable
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a pipe to be applied before an expectation is checked.
|
||||
*/
|
||||
public static function pipe(string $name, Closure $handler): void
|
||||
{
|
||||
self::$pipes[$name][] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an interceptor that should replace an existing expectation.
|
||||
*
|
||||
* @param class-string|Closure(mixed $value, mixed ...$arguments): bool $filter
|
||||
* @param Closure(mixed ...$arguments): void $handler
|
||||
*/
|
||||
public static function intercept(string $name, string|Closure $filter, Closure $handler): void
|
||||
{
|
||||
if (is_string($filter)) {
|
||||
$filter = fn ($value, ...$arguments): bool => $value instanceof $filter;
|
||||
}
|
||||
|
||||
self::pipe($name, function ($next, ...$arguments) use ($handler, $filter): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (!$filter($this->value, ...$arguments)) {
|
||||
$next();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$handler = $handler->bindTo($this, $this::class);
|
||||
|
||||
$handler(...$arguments);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pipes that have been registered for a given expectation and binds them to a context and a scope.
|
||||
*
|
||||
* @return array<int, Closure>
|
||||
*/
|
||||
private function pipes(string $name, object $context, string $scope): array
|
||||
|
||||
@ -5,11 +5,9 @@ declare(strict_types=1);
|
||||
namespace Pest\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Pest\Support\Backtrace;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\ExecutionOrderDependency;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@ -17,11 +15,6 @@ use Throwable;
|
||||
*/
|
||||
trait Testable
|
||||
{
|
||||
/**
|
||||
* The Test Case description.
|
||||
*/
|
||||
private string $__description;
|
||||
|
||||
/**
|
||||
* The Test Case "test" closure.
|
||||
*/
|
||||
@ -48,46 +41,22 @@ trait Testable
|
||||
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::$__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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$this->__test = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($name)->getClosure($this);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -148,16 +117,6 @@ trait Testable
|
||||
: $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.
|
||||
*/
|
||||
@ -234,26 +193,14 @@ trait Testable
|
||||
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.
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -8,6 +8,7 @@ use BadMethodCallException;
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use Pest\Concerns\RetrievesValues;
|
||||
use Pest\Exceptions\InvalidExpectationValue;
|
||||
use Pest\Support\Arr;
|
||||
use Pest\Support\NullClosure;
|
||||
use PHPUnit\Framework\Assert;
|
||||
@ -34,7 +35,7 @@ final class CoreExpectation
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
private ?Exporter $exporter = null;
|
||||
private Exporter|null $exporter = null;
|
||||
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
@ -156,8 +157,12 @@ final class CoreExpectation
|
||||
{
|
||||
foreach ($needles as $needle) {
|
||||
if (is_string($this->value)) {
|
||||
Assert::assertStringContainsString($needle, $this->value);
|
||||
// @phpstan-ignore-next-line
|
||||
Assert::assertStringContainsString((string) $needle, $this->value);
|
||||
} else {
|
||||
if (!is_iterable($this->value)) {
|
||||
InvalidExpectationValue::expected('iterable');
|
||||
}
|
||||
Assert::assertContains($needle, $this->value);
|
||||
}
|
||||
}
|
||||
@ -167,20 +172,32 @@ final class CoreExpectation
|
||||
|
||||
/**
|
||||
* Asserts that the value starts with $expected.
|
||||
*
|
||||
* @param non-empty-string $expected
|
||||
*/
|
||||
public function toStartWith(string $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertStringStartsWith($expected, $this->value); //@phpstan-ignore-line
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertStringStartsWith($expected, $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the value ends with $expected.
|
||||
*
|
||||
* @param non-empty-string $expected
|
||||
*/
|
||||
public function toEndWith(string $expected): CoreExpectation
|
||||
{
|
||||
Assert::assertStringEndsWith($expected, $this->value); //@phpstan-ignore-line
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertStringEndsWith($expected, $this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -212,7 +229,7 @@ final class CoreExpectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('CoreExpectation value length is not countable.');
|
||||
throw new BadMethodCallException('Expectation value length is not countable.');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,6 +237,10 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toHaveCount(int $count): CoreExpectation
|
||||
{
|
||||
if (!is_countable($this->value) && !is_iterable($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertCount($count, $this->value);
|
||||
|
||||
return $this;
|
||||
@ -232,6 +253,7 @@ final class CoreExpectation
|
||||
{
|
||||
$this->toBeObject();
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
Assert::assertTrue(property_exists($this->value, $name));
|
||||
|
||||
if (func_num_args() > 1) {
|
||||
@ -443,6 +465,8 @@ final class CoreExpectation
|
||||
public function toBeJson(): CoreExpectation
|
||||
{
|
||||
Assert::assertIsString($this->value);
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
Assert::assertJson($this->value);
|
||||
|
||||
return $this;
|
||||
@ -513,6 +537,10 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toBeDirectory(): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertDirectoryExists($this->value);
|
||||
|
||||
return $this;
|
||||
@ -523,6 +551,10 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toBeReadableDirectory(): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertDirectoryIsReadable($this->value);
|
||||
|
||||
return $this;
|
||||
@ -533,6 +565,10 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toBeWritableDirectory(): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertDirectoryIsWritable($this->value);
|
||||
|
||||
return $this;
|
||||
@ -543,6 +579,10 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toBeFile(): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertFileExists($this->value);
|
||||
|
||||
return $this;
|
||||
@ -553,6 +593,10 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toBeReadableFile(): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
Assert::assertFileIsReadable($this->value);
|
||||
|
||||
return $this;
|
||||
@ -563,6 +607,9 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toBeWritableFile(): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
Assert::assertFileIsWritable($this->value);
|
||||
|
||||
return $this;
|
||||
@ -607,6 +654,10 @@ final class CoreExpectation
|
||||
public function toMatchObject(iterable|object $object): CoreExpectation
|
||||
{
|
||||
foreach ((array) $object as $property => $value) {
|
||||
if (!is_object($this->value) && !is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('object|string');
|
||||
}
|
||||
|
||||
Assert::assertTrue(property_exists($this->value, $property));
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
@ -630,6 +681,9 @@ final class CoreExpectation
|
||||
*/
|
||||
public function toMatch(string $expression): CoreExpectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
Assert::assertMatchesRegularExpression($expression, $this->value);
|
||||
|
||||
return $this;
|
||||
|
||||
103
src/Datasets.php
103
src/Datasets.php
@ -7,7 +7,9 @@ namespace Pest;
|
||||
use Closure;
|
||||
use Pest\Exceptions\DatasetAlreadyExist;
|
||||
use Pest\Exceptions\DatasetDoesNotExist;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use function sprintf;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
@ -18,10 +20,17 @@ final class Datasets
|
||||
/**
|
||||
* Holds the datasets.
|
||||
*
|
||||
* @var array<int|string, Closure|iterable<int|string, mixed>>
|
||||
* @var array<string, Closure|iterable<int|string, mixed>>
|
||||
*/
|
||||
private static array $datasets = [];
|
||||
|
||||
/**
|
||||
* Holds the withs.
|
||||
*
|
||||
* @var array<array<string, Closure|iterable<int|string, mixed>|string>>
|
||||
*/
|
||||
private static array $withs = [];
|
||||
|
||||
/**
|
||||
* Sets the given.
|
||||
*
|
||||
@ -37,65 +46,83 @@ final class Datasets
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Closure|iterable<int|string, mixed>
|
||||
* Sets the given "with".
|
||||
*
|
||||
* @param array<Closure|iterable<int|string, mixed>|string> $with
|
||||
*/
|
||||
public static function get(string $name): Closure|iterable
|
||||
public static function with(string $filename, string $description, array $with): void
|
||||
{
|
||||
if (!array_key_exists($name, self::$datasets)) {
|
||||
throw new DatasetDoesNotExist($name);
|
||||
self::$withs[$filename . '>>>' . $description] = $with;
|
||||
}
|
||||
|
||||
return self::$datasets[$name];
|
||||
/**
|
||||
* @return Closure|iterable<int|string, mixed>|never
|
||||
*
|
||||
* @throws ShouldNotHappen
|
||||
*/
|
||||
public static function get(string $filename, string $description): Closure|iterable
|
||||
{
|
||||
$dataset = self::$withs[$filename . '>>>' . $description];
|
||||
|
||||
$dataset = self::resolve($description, $dataset);
|
||||
|
||||
if ($dataset === null) {
|
||||
throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.');
|
||||
}
|
||||
|
||||
return $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 = [];
|
||||
$datasetDescriptions = [];
|
||||
$datasetValues = [];
|
||||
|
||||
foreach ($datasetCombinations as $datasetCombination) {
|
||||
$partialDescriptions = [];
|
||||
$values = [];
|
||||
|
||||
foreach ($datasetCombination as $dataset_data) {
|
||||
$partialDescriptions[] = $dataset_data['label'];
|
||||
$values = array_merge($values, $dataset_data['values']);
|
||||
foreach ($datasetCombination as $datasetCombinationElement) {
|
||||
$partialDescriptions[] = $datasetCombinationElement['label'];
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$values = array_merge($values, $datasetCombinationElement['values']);
|
||||
}
|
||||
|
||||
$dataSetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions);
|
||||
$dataSetValues[] = $values;
|
||||
$datasetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions);
|
||||
$datasetValues[] = $values;
|
||||
}
|
||||
|
||||
foreach (array_count_values($dataSetDescriptions) as $descriptionToCheck => $count) {
|
||||
foreach (array_count_values($datasetDescriptions) as $descriptionToCheck => $count) {
|
||||
if ($count > 1) {
|
||||
$index = 1;
|
||||
foreach ($dataSetDescriptions as $i => $dataSetDescription) {
|
||||
if ($dataSetDescription === $descriptionToCheck) {
|
||||
$dataSetDescriptions[$i] .= sprintf(' #%d', $index++);
|
||||
foreach ($datasetDescriptions as $i => $datasetDescription) {
|
||||
if ($datasetDescription === $descriptionToCheck) {
|
||||
$datasetDescriptions[$i] .= sprintf(' #%d', $index++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$namedData = [];
|
||||
foreach ($dataSetDescriptions as $i => $dataSetDescription) {
|
||||
$namedData[$dataSetDescription] = $dataSetValues[$i];
|
||||
foreach ($datasetDescriptions as $i => $datasetDescription) {
|
||||
$namedData[$datasetDescription] = $datasetValues[$i];
|
||||
}
|
||||
|
||||
return $namedData;
|
||||
@ -104,7 +131,7 @@ final class Datasets
|
||||
/**
|
||||
* @param array<Closure|iterable<int|string, mixed>|string> $datasets
|
||||
*
|
||||
* @return array<array>
|
||||
* @return array<array<mixed>>
|
||||
*/
|
||||
private static function processDatasets(array $datasets): array
|
||||
{
|
||||
@ -114,7 +141,11 @@ final class Datasets
|
||||
$processedDataset = [];
|
||||
|
||||
if (is_string($data)) {
|
||||
$datasets[$index] = self::get($data);
|
||||
if (!array_key_exists($data, self::$datasets)) {
|
||||
throw new DatasetDoesNotExist($data);
|
||||
}
|
||||
|
||||
$datasets[$index] = self::$datasets[$data];
|
||||
}
|
||||
|
||||
if (is_callable($datasets[$index])) {
|
||||
@ -125,10 +156,11 @@ final class Datasets
|
||||
$datasets[$index] = iterator_to_array($datasets[$index]);
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
foreach ($datasets[$index] as $key => $values) {
|
||||
$values = is_array($values) ? $values : [$values];
|
||||
$processedDataset[] = [
|
||||
'label' => self::getDataSetDescription($key, $values),
|
||||
'label' => self::getDatasetDescription($key, $values), //@phpstan-ignore-line
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
@ -140,11 +172,11 @@ final class Datasets
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array> $combinations
|
||||
* @param array<array<mixed>> $combinations
|
||||
*
|
||||
* @return array<array>
|
||||
* @return array<array<array<mixed>>>
|
||||
*/
|
||||
private static function getDataSetsCombinations(array $combinations): array
|
||||
private static function getDatasetsCombinations(array $combinations): array
|
||||
{
|
||||
$result = [[]];
|
||||
foreach ($combinations as $index => $values) {
|
||||
@ -157,20 +189,21 @@ final class Datasets
|
||||
$result = $tmp;
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
private static function getDataSetDescription(int|string $key, array $data): string
|
||||
private static function getDatasetDescription(int|string $key, array $data): string
|
||||
{
|
||||
$exporter = new Exporter();
|
||||
|
||||
if (is_int($key)) {
|
||||
return \sprintf('(%s)', $exporter->shortenedRecursiveExport($data));
|
||||
return sprintf('(%s)', $exporter->shortenedRecursiveExport($data));
|
||||
}
|
||||
|
||||
return \sprintf('data set "%s"', $key);
|
||||
return sprintf('data set "%s"', $key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
23
src/Exceptions/InvalidExpectationValue.php
Normal file
23
src/Exceptions/InvalidExpectationValue.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvalidExpectationValue extends InvalidArgumentException
|
||||
{
|
||||
/**
|
||||
* @return never
|
||||
*
|
||||
* @throws self
|
||||
*/
|
||||
public static function expected(string $type): void
|
||||
{
|
||||
throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type));
|
||||
}
|
||||
}
|
||||
15
src/Exceptions/PipeException.php
Normal file
15
src/Exceptions/PipeException.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class PipeException extends Exception
|
||||
{
|
||||
public static function expectationNotFound(string $expectationName): PipeException
|
||||
{
|
||||
return new self("Expectation $expectationName was not found in Pest");
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,8 @@ use BadMethodCallException;
|
||||
use Closure;
|
||||
use Pest\Concerns\Extendable;
|
||||
use Pest\Concerns\RetrievesValues;
|
||||
use Pest\Exceptions\ExpectationNotFoundException;
|
||||
use Pest\Exceptions\InvalidExpectationValue;
|
||||
use Pest\Exceptions\PipeException;
|
||||
use Pest\Support\ExpectationPipeline;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
@ -58,17 +59,19 @@ final class Expectation
|
||||
*/
|
||||
public function json(): Expectation
|
||||
{
|
||||
if (!is_string($this->value)) {
|
||||
InvalidExpectationValue::expected('string');
|
||||
}
|
||||
|
||||
return $this->toBeJson()->and(json_decode($this->value, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the expectation value and end the script.
|
||||
*
|
||||
* @param mixed $arguments
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function dd(...$arguments): void
|
||||
public function dd(mixed ...$arguments): void
|
||||
{
|
||||
if (function_exists('dd')) {
|
||||
dd($this->value, ...$arguments);
|
||||
@ -85,7 +88,6 @@ final class Expectation
|
||||
public function ray(mixed ...$arguments): self
|
||||
{
|
||||
if (function_exists('ray')) {
|
||||
// @phpstan-ignore-next-line
|
||||
ray($this->value, ...$arguments);
|
||||
}
|
||||
|
||||
@ -148,7 +150,7 @@ final class Expectation
|
||||
}
|
||||
|
||||
foreach ($values as $key => $item) {
|
||||
if (is_callable($callbacks[$key])) {
|
||||
if ($callbacks[$key] instanceof Closure) {
|
||||
call_user_func($callbacks[$key], new self($item), new self($keys[$key]));
|
||||
continue;
|
||||
}
|
||||
@ -244,10 +246,8 @@ final class Expectation
|
||||
* creates a new higher order expectation.
|
||||
*
|
||||
* @param array<int, mixed> $parameters
|
||||
*
|
||||
* @return HigherOrderExpectation|mixed
|
||||
*/
|
||||
public function __call(string $method, array $parameters)
|
||||
public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation
|
||||
{
|
||||
if (!$this->hasExpectation($method)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
@ -262,22 +262,22 @@ final class Expectation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically calls methods on the class without any arguments
|
||||
* or creates a new higher order expectation.
|
||||
*/
|
||||
public function __get(string $name): mixed
|
||||
private function getExpectationClosure(string $name): Closure
|
||||
{
|
||||
if ($name === 'value') {
|
||||
return $this->coreExpectation->value;
|
||||
if (method_exists($this->coreExpectation, $name)) {
|
||||
//@phpstan-ignore-next-line
|
||||
return Closure::fromCallable([$this->coreExpectation, $name]);
|
||||
}
|
||||
|
||||
if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !self::hasExtend($name)) {
|
||||
return new HigherOrderExpectation($this, $this->retrieve($name, $this->value));
|
||||
if (self::hasExtend($name)) {
|
||||
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
|
||||
|
||||
if ($extend != false) {
|
||||
return $extend;
|
||||
}
|
||||
}
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
return $this->{$name}();
|
||||
throw PipeException::expectationNotFound($name);
|
||||
}
|
||||
|
||||
private function hasExpectation(string $name): bool
|
||||
@ -293,22 +293,24 @@ final class Expectation
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getExpectationClosure(string $name): Closure
|
||||
/**
|
||||
* Dynamically calls methods on the class without any arguments
|
||||
* or creates a new higher order expectation.
|
||||
*
|
||||
* @return Expectation|OppositeExpectation|Each|HigherOrderExpectation|TValue
|
||||
*/
|
||||
public function __get(string $name)
|
||||
{
|
||||
if (method_exists($this->coreExpectation, $name)) {
|
||||
if ($name === 'value') {
|
||||
return $this->coreExpectation->value;
|
||||
}
|
||||
|
||||
if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !Expectation::hasExtend($name)) {
|
||||
return new HigherOrderExpectation($this, $this->retrieve($name, $this->value));
|
||||
}
|
||||
|
||||
/* @phpstan-ignore-next-line */
|
||||
return Closure::fromCallable([$this->coreExpectation, $name]);
|
||||
}
|
||||
|
||||
if (self::hasExtend($name)) {
|
||||
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
|
||||
|
||||
if ($extend != false) {
|
||||
return $extend;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ExpectationNotFoundException($name);
|
||||
return $this->{$name}();
|
||||
}
|
||||
|
||||
public static function hasMethod(string $name): bool
|
||||
|
||||
32
src/Factories/Annotations/Depends.php
Normal file
32
src/Factories/Annotations/Depends.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?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.
|
||||
*
|
||||
* @param array<int, string> $annotations
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
|
||||
{
|
||||
foreach ($method->depends as $depend) {
|
||||
$depend = Str::evaluable($depend);
|
||||
|
||||
$annotations[] = "@depends $depend";
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
}
|
||||
29
src/Factories/Annotations/Groups.php
Normal file
29
src/Factories/Annotations/Groups.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories\Annotations;
|
||||
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Groups
|
||||
{
|
||||
/**
|
||||
* Adds annotations regarding the "groups" feature.
|
||||
*
|
||||
* @param array<int, string> $annotations
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function __invoke(TestCaseMethodFactory $method, array $annotations): array
|
||||
{
|
||||
foreach ($method->groups as $group) {
|
||||
$annotations[] = "@group $group";
|
||||
}
|
||||
|
||||
return $annotations;
|
||||
}
|
||||
}
|
||||
35
src/Factories/Concerns/HigherOrderable.php
Normal file
35
src/Factories/Concerns/HigherOrderable.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories\Concerns;
|
||||
|
||||
use Pest\Support\HigherOrderMessageCollection;
|
||||
|
||||
trait HigherOrderable
|
||||
{
|
||||
/**
|
||||
* The higher order messages that are chainable.
|
||||
*/
|
||||
public HigherOrderMessageCollection $chains;
|
||||
|
||||
/**
|
||||
* The higher order messages that are "factory" proxyable.
|
||||
*/
|
||||
public HigherOrderMessageCollection $factoryProxies;
|
||||
|
||||
/**
|
||||
* The higher order messages that are proxyable.
|
||||
*/
|
||||
public HigherOrderMessageCollection $proxies;
|
||||
|
||||
/**
|
||||
* Boot the higher order properties.
|
||||
*/
|
||||
private function bootHigherOrderable(): void
|
||||
{
|
||||
$this->chains = new HigherOrderMessageCollection();
|
||||
$this->factoryProxies = new HigherOrderMessageCollection();
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
}
|
||||
}
|
||||
@ -4,16 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories;
|
||||
|
||||
use Closure;
|
||||
use ParseError;
|
||||
use Pest\Concerns;
|
||||
use Pest\Contracts\HasPrintableTestCaseName;
|
||||
use Pest\Datasets;
|
||||
use Pest\Exceptions\DatasetMissing;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Support\HigherOrderMessageCollection;
|
||||
use Pest\Exceptions\TestAlreadyExist;
|
||||
use Pest\Factories\Concerns\HigherOrderable;
|
||||
use Pest\Plugins\Environment;
|
||||
use Pest\Support\Reflection;
|
||||
use Pest\Support\Str;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
@ -22,22 +24,17 @@ use RuntimeException;
|
||||
*/
|
||||
final class TestCaseFactory
|
||||
{
|
||||
/**
|
||||
* Determines if the Test Case will be the "only" being run.
|
||||
*/
|
||||
public bool $only = false;
|
||||
use HigherOrderable;
|
||||
|
||||
/**
|
||||
* The Test Case closure.
|
||||
*/
|
||||
public Closure $test;
|
||||
|
||||
/**
|
||||
* The Test Case Dataset, if any.
|
||||
* The list of annotations.
|
||||
*
|
||||
* @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.
|
||||
@ -47,7 +44,14 @@ final class TestCaseFactory
|
||||
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>
|
||||
*/
|
||||
@ -56,81 +60,52 @@ final class TestCaseFactory
|
||||
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.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
public ?string $description,
|
||||
Closure $closure = null)
|
||||
{
|
||||
$this->test = $closure ?? fn () => Assert::getCount() > 0 ?: self::markTestIncomplete();
|
||||
public string $filename
|
||||
) {
|
||||
$this->bootHigherOrderable();
|
||||
}
|
||||
|
||||
$this->factoryProxies = new HigherOrderMessageCollection();
|
||||
$this->proxies = new HigherOrderMessageCollection();
|
||||
$this->chains = new HigherOrderMessageCollection();
|
||||
public function make(): void
|
||||
{
|
||||
$methodsUsingOnly = $this->methodsUsingOnly();
|
||||
|
||||
$methods = array_values(array_filter($this->methods, function ($method) use ($methodsUsingOnly) {
|
||||
return count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true);
|
||||
}));
|
||||
|
||||
if (count($methods) > 0) {
|
||||
$this->evaluate($this->filename, $methods);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
throw ShouldNotHappen::fromMessage('Description can not be empty.');
|
||||
if (Environment::name() === Environment::CI) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$chains = $this->chains;
|
||||
$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);
|
||||
return array_values(array_filter($this->methods, static fn ($method): bool => $method->only));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a Fully Qualified Class Name from the given filename.
|
||||
* Creates a Test Case class using a runtime evaluate.
|
||||
*
|
||||
* @param array<int, TestCaseMethodFactory> $methods
|
||||
*/
|
||||
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', 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)));
|
||||
@ -152,7 +127,9 @@ final class TestCaseFactory
|
||||
}
|
||||
|
||||
$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);
|
||||
$className = array_pop($partsFQN);
|
||||
@ -164,14 +141,70 @@ final class TestCaseFactory
|
||||
$classFQN .= $className;
|
||||
}
|
||||
|
||||
$methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string {
|
||||
if ($method->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||
}
|
||||
|
||||
$methodName = Str::evaluable($method->description);
|
||||
|
||||
$datasetsCode = '';
|
||||
$annotations = ['@test'];
|
||||
|
||||
foreach (self::$annotations as $annotation) {
|
||||
/** @phpstan-ignore-next-line */
|
||||
$annotations = (new $annotation())->__invoke($method, $annotations);
|
||||
}
|
||||
|
||||
if (count($method->datasets) > 0) {
|
||||
$dataProviderName = $methodName . '_dataset';
|
||||
$annotations[] = "@dataProvider $dataProviderName";
|
||||
|
||||
Datasets::with($method->filename, $methodName, $method->datasets);
|
||||
|
||||
$datasetsCode = <<<EOF
|
||||
|
||||
public function $dataProviderName()
|
||||
{
|
||||
return __PestDatasets::get(self::\$__filename, "$methodName");
|
||||
}
|
||||
|
||||
EOF;
|
||||
}
|
||||
|
||||
$annotations = implode('', array_map(
|
||||
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
|
||||
));
|
||||
|
||||
return <<<EOF
|
||||
|
||||
/**$annotations
|
||||
*/
|
||||
public function $methodName()
|
||||
{
|
||||
return \$this->__runTest(
|
||||
\$this->__test,
|
||||
...func_get_args(),
|
||||
);
|
||||
}
|
||||
|
||||
$datasetsCode
|
||||
EOF;
|
||||
}, $methods));
|
||||
|
||||
try {
|
||||
eval("
|
||||
namespace $namespace;
|
||||
|
||||
use Pest\Datasets as __PestDatasets;
|
||||
use Pest\TestSuite as __PestTestSuite;
|
||||
|
||||
final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN {
|
||||
$traitsCode
|
||||
|
||||
private static \$__filename = '$filename';
|
||||
|
||||
$methodsCode
|
||||
}
|
||||
");
|
||||
} catch (ParseError $caught) {
|
||||
@ -182,11 +215,48 @@ final class TestCaseFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test case will receive argument input from Pest, or not.
|
||||
* Adds the given Method to the Test Case.
|
||||
*/
|
||||
public function __receivesArguments(): bool
|
||||
public function addMethod(TestCaseMethodFactory $method): void
|
||||
{
|
||||
return count($this->datasets) > 0
|
||||
|| $this->factoryProxies->count('addDependencies') > 0;
|
||||
if ($method->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||
}
|
||||
|
||||
if (array_key_exists($method->description, $this->methods)) {
|
||||
throw new TestAlreadyExist($method->filename, $method->description);
|
||||
}
|
||||
|
||||
if (!$method->receivesArguments()) {
|
||||
if ($method->closure === null) {
|
||||
throw ShouldNotHappen::fromMessage('The test closure may not be empty.');
|
||||
}
|
||||
|
||||
$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 ($method->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('The test description may not be empty.');
|
||||
}
|
||||
|
||||
if (Str::evaluable($method->description) === $methodName) {
|
||||
return $method;
|
||||
}
|
||||
}
|
||||
|
||||
throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName));
|
||||
}
|
||||
}
|
||||
|
||||
103
src/Factories/TestCaseMethodFactory.php
Normal file
103
src/Factories/TestCaseMethodFactory.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Factories;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
use Pest\Factories\Concerns\HigherOrderable;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class TestCaseMethodFactory
|
||||
{
|
||||
use HigherOrderable;
|
||||
/**
|
||||
* Determines if the Test Case will be the "only" being run.
|
||||
*/
|
||||
public bool $only = false;
|
||||
|
||||
/**
|
||||
* The Test Case Dataset, if any.
|
||||
*
|
||||
* @var array<Closure|iterable<int|string, mixed>|string>
|
||||
*/
|
||||
public array $datasets = [];
|
||||
|
||||
/**
|
||||
* The Test Case depends, if any.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $depends = [];
|
||||
|
||||
/**
|
||||
* The Test Case groups, if any.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $groups = [];
|
||||
|
||||
/**
|
||||
* Creates a new Factory instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
public ?string $description,
|
||||
public ?Closure $closure,
|
||||
) {
|
||||
if ($this->closure === null) {
|
||||
$this->closure = function () {
|
||||
Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line
|
||||
};
|
||||
}
|
||||
|
||||
$this->bootHigherOrderable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the Test Case classes.
|
||||
*/
|
||||
public function getClosure(TestCase $concrete): Closure
|
||||
{
|
||||
$concrete::flush(); // @phpstan-ignore-line
|
||||
|
||||
if ($this->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('Description can not be empty.');
|
||||
}
|
||||
|
||||
$closure = $this->closure;
|
||||
|
||||
$testCase = TestSuite::getInstance()->tests->get($this->filename);
|
||||
|
||||
$testCase->factoryProxies->proxy($concrete);
|
||||
$this->factoryProxies->proxy($concrete);
|
||||
|
||||
$method = $this;
|
||||
|
||||
return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
|
||||
/* @var TestCase $this */
|
||||
|
||||
$testCase->proxies->proxy($this);
|
||||
$method->proxies->proxy($this);
|
||||
|
||||
$testCase->chains->chain($this);
|
||||
$method->chains->chain($this);
|
||||
|
||||
return \Pest\Support\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;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ use Pest\Support\HigherOrderTapProxy;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
if (!function_exists('expect')) {
|
||||
/**
|
||||
* Creates a new expectation.
|
||||
*
|
||||
@ -27,6 +28,7 @@ function expect($value = null): Expectation|Extendable
|
||||
|
||||
return new Expectation($value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('beforeAll')) {
|
||||
/**
|
||||
@ -68,12 +70,14 @@ if (!function_exists('uses')) {
|
||||
/**
|
||||
* The uses function binds the given
|
||||
* arguments to test closures.
|
||||
*
|
||||
* @param class-string ...$classAndTraits
|
||||
*/
|
||||
function uses(string ...$classAndTraits): UsesCall
|
||||
{
|
||||
$filename = Backtrace::file();
|
||||
|
||||
return new UsesCall($filename, $classAndTraits);
|
||||
return new UsesCall($filename, array_values($classAndTraits));
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +113,10 @@ if (!function_exists('it')) {
|
||||
{
|
||||
$description = sprintf('it %s', $description);
|
||||
|
||||
return test($description, $closure);
|
||||
/** @var TestCall $test */
|
||||
$test = test($description, $closure);
|
||||
|
||||
return $test;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -80,7 +80,10 @@ final class HigherOrderExpectation
|
||||
}
|
||||
|
||||
if (!$this->expectationHasMethod($name)) {
|
||||
return new self($this->original, $this->retrieve($name, $this->getValue()));
|
||||
/** @var array<string, mixed>|object $value */
|
||||
$value = $this->getValue();
|
||||
|
||||
return new self($this->original, $this->retrieve($name, $value));
|
||||
}
|
||||
|
||||
return $this->performAssertion($name, []);
|
||||
|
||||
15
src/IgnorableTestCase.php
Normal file
15
src/IgnorableTestCase.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class IgnorableTestCase extends TestCase
|
||||
{
|
||||
// ..
|
||||
}
|
||||
@ -18,7 +18,6 @@ final class Kernel
|
||||
*/
|
||||
private static array $bootstrappers = [
|
||||
Bootstrappers\BootExceptionHandler::class,
|
||||
Bootstrappers\BootEmitter::class,
|
||||
Bootstrappers\BootSubscribers::class,
|
||||
Bootstrappers\BootFiles::class,
|
||||
];
|
||||
@ -38,6 +37,7 @@ final class Kernel
|
||||
public static function boot(): self
|
||||
{
|
||||
foreach (self::$bootstrappers as $bootstrapper) {
|
||||
//@phpstan-ignore-next-line
|
||||
(new $bootstrapper())->__invoke();
|
||||
}
|
||||
|
||||
@ -65,6 +65,6 @@ final class Kernel
|
||||
*/
|
||||
public function shutdown(): void
|
||||
{
|
||||
// TODO
|
||||
// ..
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,403 +12,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Logging;
|
||||
|
||||
use function class_exists;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Exception;
|
||||
use function method_exists;
|
||||
use Pest\Concerns\Testable;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\ExceptionWrapper;
|
||||
use PHPUnit\Framework\SelfDescribing;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestFailure;
|
||||
use PHPUnit\Framework\TestListener;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\Util\Filter;
|
||||
use PHPUnit\Util\Printer;
|
||||
use PHPUnit\Util\Xml;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use Throwable;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||
*/
|
||||
final class JUnit extends Printer implements TestListener
|
||||
final class JUnit extends Printer
|
||||
{
|
||||
private DOMDocument $document;
|
||||
|
||||
private DOMElement $root;
|
||||
|
||||
/**
|
||||
* @var array<int, DOMElement>
|
||||
*/
|
||||
private array $testSuites = [];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $testSuiteTests = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $testSuiteAssertions = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $testSuiteErrors = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $testSuiteWarnings = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $testSuiteFailures = [0];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
*/
|
||||
private array $testSuiteSkipped = [0];
|
||||
|
||||
private array $testSuiteTimes = [0];
|
||||
|
||||
private int $testSuiteLevel = 0;
|
||||
|
||||
private ?DOMElement $currentTestCase = null;
|
||||
|
||||
public function __construct(string $out)
|
||||
{
|
||||
$this->document = new DOMDocument('1.0', 'UTF-8');
|
||||
$this->document->formatOutput = true;
|
||||
|
||||
$this->root = $this->document->createElement('testsuites');
|
||||
$this->document->appendChild($this->root);
|
||||
|
||||
parent::__construct($out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush buffer and close output.
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
$this->write($this->getXML());
|
||||
|
||||
parent::flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* An error occurred.
|
||||
*/
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $t, 'error');
|
||||
$this->testSuiteErrors[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* A warning occurred.
|
||||
*/
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $e, 'warning');
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* A failure occurred.
|
||||
*/
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->doAddFault($test, $e, 'failure');
|
||||
$this->testSuiteFailures[$this->testSuiteLevel]++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incomplete test.
|
||||
*/
|
||||
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* Risky test.
|
||||
*/
|
||||
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Skipped test.
|
||||
*/
|
||||
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->doAddSkipped();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$testSuite = $this->document->createElement('testsuite');
|
||||
$testSuite->setAttribute('name', $suite->getName());
|
||||
|
||||
if (class_exists($suite->getName(), false)) {
|
||||
try {
|
||||
$class = new ReflectionClass($suite->getName());
|
||||
|
||||
if ($class->hasMethod('__getFileName')) {
|
||||
$fileName = $class->getMethod('__getFileName')->invoke(null);
|
||||
} else {
|
||||
$fileName = $class->getFileName();
|
||||
}
|
||||
|
||||
$testSuite->setAttribute('file', $fileName);
|
||||
} catch (ReflectionException) {
|
||||
// @ignoreException
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->testSuiteLevel > 0) {
|
||||
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
|
||||
} else {
|
||||
$this->root->appendChild($testSuite);
|
||||
}
|
||||
|
||||
$this->testSuiteLevel++;
|
||||
$this->testSuites[$this->testSuiteLevel] = $testSuite;
|
||||
$this->testSuiteTests[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
|
||||
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'tests',
|
||||
(string) $this->testSuiteTests[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'assertions',
|
||||
(string) $this->testSuiteAssertions[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'errors',
|
||||
(string) $this->testSuiteErrors[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'warnings',
|
||||
(string) $this->testSuiteWarnings[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'failures',
|
||||
(string) $this->testSuiteFailures[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'skipped',
|
||||
(string) $this->testSuiteSkipped[$this->testSuiteLevel]
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->setAttribute(
|
||||
'time',
|
||||
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel])
|
||||
);
|
||||
|
||||
if ($this->testSuiteLevel > 1) {
|
||||
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
|
||||
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
|
||||
$this->testSuiteWarnings[$this->testSuiteLevel - 1] += $this->testSuiteWarnings[$this->testSuiteLevel];
|
||||
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
|
||||
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
|
||||
}
|
||||
|
||||
$this->testSuiteLevel--;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test started.
|
||||
*
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
$usesDataprovider = false;
|
||||
|
||||
if (method_exists($test, 'usesDataProvider')) {
|
||||
$usesDataprovider = $test->usesDataProvider();
|
||||
}
|
||||
|
||||
$testCase = $this->document->createElement('testcase');
|
||||
$testCase->setAttribute('name', $test->getName());
|
||||
|
||||
try {
|
||||
$class = new ReflectionClass($test);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
// @phpstan-ignore-next-line
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$methodName = $test->getName(!$usesDataprovider);
|
||||
|
||||
if ($class->hasMethod($methodName)) {
|
||||
try {
|
||||
$method = $class->getMethod($methodName);
|
||||
// @codeCoverageIgnoreStart
|
||||
} catch (ReflectionException $e) {
|
||||
// @phpstan-ignore-next-line
|
||||
throw new Exception($e->getMessage(), (int) $e->getCode(), $e);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$testCase->setAttribute('class', $class->getName());
|
||||
$testCase->setAttribute('classname', str_replace('\\', '.', $class->getName()));
|
||||
$fileName = $class->getFileName();
|
||||
if ($fileName !== false) {
|
||||
$testCase->setAttribute('file', $fileName);
|
||||
}
|
||||
$testCase->setAttribute('line', (string) $method->getStartLine());
|
||||
}
|
||||
|
||||
if (TeamCity::isPestTest($test)) {
|
||||
$testCase->setAttribute('class', $test->getPrintableTestCaseName());
|
||||
$testCase->setAttribute('classname', str_replace('\\', '.', $test->getPrintableTestCaseName()));
|
||||
// @phpstan-ignore-next-line
|
||||
$testCase->setAttribute('file', $test->__getFilename());
|
||||
}
|
||||
|
||||
$this->currentTestCase = $testCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test ended.
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
$numAssertions = 0;
|
||||
|
||||
if (method_exists($test, 'getNumAssertions')) {
|
||||
$numAssertions = $test->getNumAssertions();
|
||||
}
|
||||
|
||||
$this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
|
||||
|
||||
if ($this->currentTestCase !== null) {
|
||||
$this->currentTestCase->setAttribute(
|
||||
'assertions',
|
||||
(string) $numAssertions
|
||||
);
|
||||
|
||||
$this->currentTestCase->setAttribute(
|
||||
'time',
|
||||
sprintf('%F', $time)
|
||||
);
|
||||
|
||||
$this->testSuites[$this->testSuiteLevel]->appendChild(
|
||||
$this->currentTestCase
|
||||
);
|
||||
}
|
||||
|
||||
$this->testSuiteTests[$this->testSuiteLevel]++;
|
||||
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
|
||||
|
||||
$testOutput = '';
|
||||
|
||||
if (method_exists($test, 'hasOutput') && method_exists($test, 'getActualOutput')) {
|
||||
$testOutput = $test->hasOutput() ? $test->getActualOutput() : '';
|
||||
}
|
||||
|
||||
if ($testOutput !== '') {
|
||||
$systemOut = $this->document->createElement(
|
||||
'system-out',
|
||||
Xml::prepareString($testOutput)
|
||||
);
|
||||
|
||||
if ($this->currentTestCase !== null) {
|
||||
$this->currentTestCase->appendChild($systemOut);
|
||||
}
|
||||
}
|
||||
|
||||
$this->currentTestCase = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XML as a string.
|
||||
*/
|
||||
public function getXML(): string
|
||||
{
|
||||
$xml = $this->document->saveXML();
|
||||
if ($xml === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function doAddFault(Test $test, Throwable $t, string $type): void
|
||||
{
|
||||
if ($this->currentTestCase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($test instanceof SelfDescribing) {
|
||||
$buffer = $test->toString() . "\n";
|
||||
} else {
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
$buffer .= trim(
|
||||
TestFailure::exceptionToString($t) . "\n" .
|
||||
Filter::getFilteredStacktrace($t)
|
||||
);
|
||||
|
||||
$fault = $this->document->createElement(
|
||||
$type,
|
||||
Xml::prepareString($buffer)
|
||||
);
|
||||
|
||||
if ($t instanceof ExceptionWrapper) {
|
||||
$fault->setAttribute('type', $t->getClassName());
|
||||
} else {
|
||||
$fault->setAttribute('type', $t::class);
|
||||
}
|
||||
|
||||
$this->currentTestCase->appendChild($fault);
|
||||
}
|
||||
|
||||
private function doAddSkipped(): void
|
||||
{
|
||||
if ($this->currentTestCase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$skipped = $this->document->createElement('skipped');
|
||||
|
||||
$this->currentTestCase->appendChild($skipped);
|
||||
|
||||
$this->testSuiteSkipped[$this->testSuiteLevel]++;
|
||||
}
|
||||
// @todo
|
||||
}
|
||||
|
||||
@ -4,289 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Logging;
|
||||
|
||||
use function getmypid;
|
||||
use Pest\Concerns\Logging\WritesToConsole;
|
||||
use Pest\Concerns\Testable;
|
||||
use Pest\Support\ExceptionTrace;
|
||||
use function Pest\version;
|
||||
use PHPUnit\Framework\AssertionFailedError;
|
||||
use PHPUnit\Framework\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\TestResult;
|
||||
use PHPUnit\Framework\TestSuite;
|
||||
use PHPUnit\Framework\Warning;
|
||||
use PHPUnit\TextUI\DefaultResultPrinter;
|
||||
use PHPUnit\TextUI\XmlConfiguration\Logging\TeamCity as BaseTeamCity;
|
||||
use function round;
|
||||
use function str_replace;
|
||||
use Throwable;
|
||||
|
||||
final class TeamCity extends DefaultResultPrinter
|
||||
{
|
||||
use WritesToConsole;
|
||||
private const PROTOCOL = 'pest_qn://';
|
||||
private const NAME = 'name';
|
||||
private const LOCATION_HINT = 'locationHint';
|
||||
private const DURATION = 'duration';
|
||||
private const TEST_SUITE_STARTED = 'testSuiteStarted';
|
||||
private const TEST_SUITE_FINISHED = 'testSuiteFinished';
|
||||
private const TEST_COUNT = 'testCount';
|
||||
private const TEST_STARTED = 'testStarted';
|
||||
private const TEST_FINISHED = 'testFinished';
|
||||
|
||||
private ?int $flowId = null;
|
||||
|
||||
private bool $isSummaryTestCountPrinted = false;
|
||||
|
||||
private BaseTeamCity $phpunitTeamCity;
|
||||
|
||||
/**
|
||||
* Creates a new printer instance.
|
||||
*/
|
||||
public function __construct(resource|string|null $out, bool $verbose, string $colors)
|
||||
{
|
||||
parent::__construct($out, $verbose, $colors);
|
||||
$this->phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors);
|
||||
|
||||
$this->logo();
|
||||
}
|
||||
|
||||
private function logo(): void
|
||||
{
|
||||
$this->writeNewLine();
|
||||
$this->write('Pest ' . version());
|
||||
$this->writeNewLine();
|
||||
}
|
||||
|
||||
public function printResult(TestResult $result): void
|
||||
{
|
||||
$this->write('Tests: ');
|
||||
|
||||
$results = [
|
||||
'failed' => ['count' => $result->errorCount() + $result->failureCount(), 'color' => 'fg-red'],
|
||||
'skipped' => ['count' => $result->skippedCount(), 'color' => 'fg-yellow'],
|
||||
'warned' => ['count' => $result->warningCount(), 'color' => 'fg-yellow'],
|
||||
'risked' => ['count' => $result->riskyCount(), 'color' => 'fg-yellow'],
|
||||
'incomplete' => ['count' => $result->notImplementedCount(), 'color' => 'fg-yellow'],
|
||||
'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'],
|
||||
];
|
||||
|
||||
$filteredResults = array_filter($results, fn ($item): bool => $item['count'] > 0);
|
||||
|
||||
foreach ($filteredResults as $key => $info) {
|
||||
$this->writeWithColor($info['color'], $info['count'] . " $key", false);
|
||||
|
||||
if ($key !== array_reverse(array_keys($filteredResults))[0]) {
|
||||
$this->write(', ');
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->write("Assertions: $this->numAssertions");
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->write("Time: {$result->time()}s");
|
||||
|
||||
$this->writeNewLine();
|
||||
}
|
||||
|
||||
private function successfulTestCount(TestResult $result): int
|
||||
{
|
||||
return $result->count()
|
||||
- $result->failureCount()
|
||||
- $result->errorCount()
|
||||
- $result->skippedCount()
|
||||
- $result->warningCount()
|
||||
- $result->notImplementedCount()
|
||||
- $result->riskyCount();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function startTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
if (static::isCompoundTestSuite($suite)) {
|
||||
$this->writeWithColor('bold', ' ' . $suiteName);
|
||||
} elseif (static::isPestTestSuite($suite)) {
|
||||
$this->writeWithColor('fg-white, bold', ' ' . substr_replace($suiteName, '', 0, 2) . ' ');
|
||||
} else {
|
||||
$this->writeWithColor('fg-white, bold', ' ' . $suiteName);
|
||||
}
|
||||
|
||||
$this->writeNewLine();
|
||||
|
||||
$this->flowId = (int) getmypid();
|
||||
|
||||
if (!$this->isSummaryTestCountPrinted) {
|
||||
$this->printEvent(self::TEST_COUNT, [
|
||||
'count' => $suite->count(),
|
||||
]);
|
||||
$this->isSummaryTestCountPrinted = true;
|
||||
}
|
||||
|
||||
$this->printEvent(self::TEST_SUITE_STARTED, [
|
||||
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|int> $params
|
||||
*/
|
||||
private function printEvent(string $eventName, array $params = []): void
|
||||
{
|
||||
$this->write("##teamcity[{$eventName}");
|
||||
|
||||
if ($this->flowId !== 0) {
|
||||
$params['flowId'] = $this->flowId;
|
||||
}
|
||||
|
||||
foreach ($params as $key => $value) {
|
||||
$escapedValue = self::escapeValue((string) $value);
|
||||
$this->write(" {$key}='{$escapedValue}'");
|
||||
}
|
||||
|
||||
$this->write("]\n");
|
||||
}
|
||||
|
||||
private static function escapeValue(string $text): string
|
||||
{
|
||||
return str_replace(
|
||||
['|', "'", "\n", "\r", ']', '['],
|
||||
['||', "|'", '|n', '|r', '|]', '|['],
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
public function endTestSuite(TestSuite $suite): void
|
||||
{
|
||||
$suiteName = $suite->getName();
|
||||
|
||||
$this->writeNewLine();
|
||||
$this->writeNewLine();
|
||||
|
||||
$this->printEvent(self::TEST_SUITE_FINISHED, [
|
||||
self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2),
|
||||
self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function startTest(Test $test): void
|
||||
{
|
||||
if (!TeamCity::isPestTest($test)) {
|
||||
$this->phpunitTeamCity->startTest($test);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->printEvent(self::TEST_STARTED, [
|
||||
self::NAME => $test->getName(),
|
||||
// @phpstan-ignore-next-line
|
||||
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the given test suite is a valid Pest suite.
|
||||
*
|
||||
* @param TestSuite<Test> $suite
|
||||
*/
|
||||
private static function isPestTestSuite(TestSuite $suite): bool
|
||||
{
|
||||
return str_starts_with($suite->getName(), 'P\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the test suite is made up of multiple smaller test suites.
|
||||
*
|
||||
* @param TestSuite<Test> $suite
|
||||
*/
|
||||
private static function isCompoundTestSuite(TestSuite $suite): bool
|
||||
{
|
||||
return file_exists($suite->getName()) || !method_exists($suite->getName(), '__getFileName');
|
||||
}
|
||||
|
||||
public static function isPestTest(Test $test): bool
|
||||
{
|
||||
/** @var array<string, string> $uses */
|
||||
$uses = class_uses($test);
|
||||
|
||||
return in_array(Testable::class, $uses, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Test|Testable $test
|
||||
*/
|
||||
public function endTest(Test $test, float $time): void
|
||||
{
|
||||
$this->printEvent(self::TEST_FINISHED, [
|
||||
self::NAME => $test->getName(),
|
||||
self::DURATION => self::toMilliseconds($time),
|
||||
]);
|
||||
|
||||
if (!$this->lastTestFailed) {
|
||||
$this->writeSuccess($test->getName());
|
||||
}
|
||||
|
||||
$this->numAssertions += $test instanceof TestCase ? $test->getNumAssertions() : 1;
|
||||
$this->lastTestFailed = false;
|
||||
}
|
||||
|
||||
private static function toMilliseconds(float $time): int
|
||||
{
|
||||
return (int) round($time * 1000);
|
||||
}
|
||||
|
||||
public function addError(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeError($test->getName());
|
||||
$this->phpunitTeamCity->addError($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addFailure(Test $test, AssertionFailedError $e, float $time): void
|
||||
{
|
||||
$this->markAsFailure($e);
|
||||
$this->writeError($test->getName());
|
||||
$this->phpunitTeamCity->addFailure($test, $e, $time);
|
||||
}
|
||||
|
||||
public function addWarning(Test $test, Warning $e, float $time): void
|
||||
{
|
||||
$this->markAsFailure($e);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addWarning($test, $e, $time);
|
||||
}
|
||||
|
||||
public function addIncompleteTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addIncompleteTest($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addRiskyTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->addRiskyTest($test, $t, $time);
|
||||
}
|
||||
|
||||
public function addSkippedTest(Test $test, Throwable $t, float $time): void
|
||||
{
|
||||
$this->markAsFailure($t);
|
||||
$this->writeWarning($test->getName());
|
||||
$this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time);
|
||||
}
|
||||
|
||||
private function markAsFailure(Throwable $t): void
|
||||
{
|
||||
$this->lastTestFailed = true;
|
||||
ExceptionTrace::removePestReferences($t);
|
||||
}
|
||||
// @todo
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ final class OppositeExpectation
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$this->original->toHaveKey($key);
|
||||
} catch (ExpectationFailedException) {
|
||||
} catch (ExpectationFailedException $exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -46,33 +46,34 @@ final class OppositeExpectation
|
||||
* Handle dynamic method calls into the original expectation.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return Expectation|never
|
||||
*/
|
||||
public function __call(string $name, array $arguments): Expectation
|
||||
{
|
||||
try {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->original->{$name}(...$arguments);
|
||||
} catch (ExpectationFailedException) {
|
||||
} catch (ExpectationFailedException $exception) {
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$this->throwExpectationFailedException($name, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic properties gets into the original expectation.
|
||||
*
|
||||
* @return Expectation|never
|
||||
*/
|
||||
public function __get(string $name): Expectation
|
||||
{
|
||||
try {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->original->{$name};
|
||||
} catch (ExpectationFailedException) {
|
||||
$this->original->{$name}; // @phpstan-ignore-line
|
||||
} catch (ExpectationFailedException $exception) { // @phpstan-ignore-line
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$this->throwExpectationFailedException($name);
|
||||
}
|
||||
|
||||
@ -80,6 +81,8 @@ final class OppositeExpectation
|
||||
* Creates a new expectation failed exception with a nice readable message.
|
||||
*
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
private function throwExpectationFailedException(string $name, array $arguments = []): void
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
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;
|
||||
@ -22,7 +22,7 @@ final class TestCall
|
||||
/**
|
||||
* The Test Case Factory.
|
||||
*/
|
||||
private TestCaseFactory $testCaseFactory;
|
||||
private TestCaseMethodFactory $testCaseMethod;
|
||||
|
||||
/**
|
||||
* If test call is descriptionLess.
|
||||
@ -38,7 +38,7 @@ final class TestCall
|
||||
string $description = null,
|
||||
Closure $closure = null
|
||||
) {
|
||||
$this->testCaseFactory = new TestCaseFactory($filename, $description, $closure);
|
||||
$this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure);
|
||||
$this->descriptionLess = $description === null;
|
||||
}
|
||||
|
||||
@ -48,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 {
|
||||
@ -56,7 +56,7 @@ final class TestCall
|
||||
}
|
||||
|
||||
if (is_string($exceptionMessage)) {
|
||||
$this->testCaseFactory
|
||||
$this->testCaseMethod
|
||||
->proxies
|
||||
->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]);
|
||||
}
|
||||
@ -74,7 +74,7 @@ final class TestCall
|
||||
$condition = is_callable($condition)
|
||||
? $condition
|
||||
: static function () use ($condition): bool {
|
||||
return $condition; // @phpstan-ignore-line
|
||||
return $condition;
|
||||
};
|
||||
|
||||
if ($condition()) {
|
||||
@ -90,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;
|
||||
@ -102,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;
|
||||
}
|
||||
@ -116,7 +116,7 @@ final class TestCall
|
||||
*/
|
||||
public function only(): TestCall
|
||||
{
|
||||
$this->testCaseFactory->only = true;
|
||||
$this->testCaseMethod->only = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -126,9 +126,9 @@ 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;
|
||||
}
|
||||
@ -153,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]);
|
||||
|
||||
@ -185,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));
|
||||
}
|
||||
@ -207,6 +207,6 @@ final class TestCall
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->testSuite->tests->set($this->testCaseFactory);
|
||||
$this->testSuite->tests->set($this->testCaseMethod);
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ final class UsesCall
|
||||
*/
|
||||
public function group(string ...$groups): UsesCall
|
||||
{
|
||||
$this->groups = $groups;
|
||||
$this->groups = array_values($groups);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ namespace Pest;
|
||||
|
||||
function version(): string
|
||||
{
|
||||
return '1.20.0';
|
||||
return '2.x-dev';
|
||||
}
|
||||
|
||||
function testDirectory(string $file = ''): string
|
||||
|
||||
@ -18,6 +18,8 @@ final class Plugin
|
||||
|
||||
/**
|
||||
* Lazy loads an `uses` call on the context of plugins.
|
||||
*
|
||||
* @param class-string ...$traits
|
||||
*/
|
||||
public static function uses(string ...$traits): void
|
||||
{
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Actions;
|
||||
|
||||
use Pest\Contracts\Plugins;
|
||||
@ -19,7 +21,7 @@ final class AddsOutput
|
||||
{
|
||||
$plugins = Loader::getPlugins(Plugins\AddsOutput::class);
|
||||
|
||||
/** @var Plugins\AddsOutpu $plugin */
|
||||
/** @var Plugins\AddsOutput $plugin */
|
||||
foreach ($plugins as $plugin) {
|
||||
$exitCode = $plugin->addOutput($exitCode);
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Actions;
|
||||
|
||||
use Pest\Contracts\Plugins;
|
||||
|
||||
@ -80,7 +80,10 @@ final class Coverage implements AddsOutput, HandlesArguments
|
||||
}
|
||||
|
||||
if ($input->getOption(self::MIN_OPTION) !== null) {
|
||||
$this->coverageMin = (float) $input->getOption(self::MIN_OPTION);
|
||||
/** @var int|float $minOption */
|
||||
$minOption = $input->getOption(self::MIN_OPTION);
|
||||
|
||||
$this->coverageMin = (float) $minOption;
|
||||
}
|
||||
|
||||
return $originals;
|
||||
|
||||
@ -41,7 +41,6 @@ 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());
|
||||
|
||||
@ -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,18 +17,13 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
final class TestRepository
|
||||
{
|
||||
/**
|
||||
* @var non-empty-string
|
||||
*/
|
||||
private const SEPARATOR = '>>>';
|
||||
|
||||
/**
|
||||
* @var array<string, TestCaseFactory>
|
||||
*/
|
||||
private array $state = [];
|
||||
private array $testCases = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<int, array<int, string|Closure>>>
|
||||
* @var array<string, array{0: array<int, string>, 1: array<int, string>, 2: array<int, string|Closure>}>
|
||||
*/
|
||||
private array $uses = [];
|
||||
|
||||
@ -42,7 +32,7 @@ final class TestRepository
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->state);
|
||||
return count($this->testCases);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,74 +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(fn (TestCaseFactory $factory): string => $factory->filename, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state));
|
||||
if (count($testCases) === 0) {
|
||||
$testCases = $this->testCases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given callable foreach test case.
|
||||
*/
|
||||
public function build(TestSuite $testSuite, callable $each): void
|
||||
{
|
||||
$startsWith = 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);
|
||||
return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $testCases));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,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(string $filename): TestCaseFactory
|
||||
{
|
||||
if ($test->description === null) {
|
||||
throw ShouldNotHappen::fromMessage('Trying to create a test without description.');
|
||||
return $this->testCases[$filename];
|
||||
}
|
||||
|
||||
if (array_key_exists(sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description), $this->state)) {
|
||||
throw new TestAlreadyExist($test->filename, $test->description);
|
||||
/**
|
||||
* Sets a new test case method.
|
||||
*/
|
||||
public function set(TestCaseMethodFactory $method): void
|
||||
{
|
||||
if (!array_key_exists($method->filename, $this->testCases)) {
|
||||
$this->testCases[$method->filename] = new TestCaseFactory($method->filename);
|
||||
}
|
||||
|
||||
if (!$test->__receivesArguments()) {
|
||||
$arguments = Reflection::getFunctionArguments($test->test);
|
||||
$this->testCases[$method->filename]->addMethod($method);
|
||||
}
|
||||
|
||||
if (count($arguments) > 0) {
|
||||
throw new DatasetMissing($test->filename, $test->description, $arguments);
|
||||
/**
|
||||
* Makes a Test Case from the given filename, if exists.
|
||||
*/
|
||||
public function makeIfExists(string $filename): void
|
||||
{
|
||||
if (array_key_exists($filename, $this->testCases)) {
|
||||
$this->make($this->testCases[$filename]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test;
|
||||
/**
|
||||
* Makes a Test Case using the given factory.
|
||||
*/
|
||||
private function make(TestCaseFactory $testCase): void
|
||||
{
|
||||
$startsWith = static fn (string $target, string $directory): bool => Str::startsWith($target, $directory . DIRECTORY_SEPARATOR);
|
||||
|
||||
foreach ($this->uses as $path => $uses) {
|
||||
[$classOrTraits, $groups, $hooks] = $uses;
|
||||
|
||||
if ((!is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) {
|
||||
foreach ($classOrTraits as $class) {
|
||||
/** @var string $class */
|
||||
if (class_exists($class)) {
|
||||
if ($testCase->class !== TestCase::class) {
|
||||
throw new TestCaseAlreadyInUse($testCase->class, $class, $testCase->filename);
|
||||
}
|
||||
$testCase->class = $class;
|
||||
} elseif (trait_exists($class)) {
|
||||
$testCase->traits[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($testCase->methods as $method) {
|
||||
foreach ($groups as $group) {
|
||||
$method->groups[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($testCase->methods as $method) {
|
||||
$method->groups = array_merge($groups, $method->groups);
|
||||
}
|
||||
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeAll', [$hooks[0] ?? null]);
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeEach', [$hooks[1] ?? null]);
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterEach', [$hooks[2] ?? null]);
|
||||
$testCase->factoryProxies->add($testCase->filename, 0, '__addAfterAll', [$hooks[3] ?? null]);
|
||||
}
|
||||
}
|
||||
|
||||
$testCase->make();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,8 @@ final class Arr
|
||||
{
|
||||
/**
|
||||
* Checks if the given array has the given key.
|
||||
*
|
||||
* @param array<array-key, mixed> $array
|
||||
*/
|
||||
public static function has(array $array, string|int $key): bool
|
||||
{
|
||||
@ -33,6 +35,8 @@ final class Arr
|
||||
|
||||
/**
|
||||
* Gets the given key value.
|
||||
*
|
||||
* @param array<array-key, mixed> $array
|
||||
*/
|
||||
public static function get(array $array, string|int $key, mixed $default = null): mixed
|
||||
{
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -17,10 +18,12 @@ final class ChainableClosure
|
||||
public static function from(Closure $closure, Closure $next): Closure
|
||||
{
|
||||
return function () use ($closure, $next): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($closure, $this, $this::class), func_get_args());
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($next, $this, $this::class), func_get_args());
|
||||
if (!is_object($this)) { // @phpstan-ignore-line
|
||||
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
|
||||
}
|
||||
|
||||
\Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args());
|
||||
\Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args());
|
||||
};
|
||||
}
|
||||
|
||||
@ -30,10 +33,8 @@ final class ChainableClosure
|
||||
public static function fromStatic(Closure $closure, Closure $next): Closure
|
||||
{
|
||||
return static function () use ($closure, $next): void {
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($closure, null, self::class), func_get_args());
|
||||
/* @phpstan-ignore-next-line */
|
||||
call_user_func_array(Closure::bind($next, null, self::class), func_get_args());
|
||||
\Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args());
|
||||
\Pest\Support\Closure::bind($next, null, self::class)(...func_get_args());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
36
src/Support/Closure.php
Normal file
36
src/Support/Closure.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Support;
|
||||
|
||||
use Closure as BaseClosure;
|
||||
use Pest\Exceptions\ShouldNotHappen;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Closure
|
||||
{
|
||||
/**
|
||||
* Binds the given closure to the given "this".
|
||||
*
|
||||
* @return BaseClosure|never
|
||||
*
|
||||
* @throws ShouldNotHappen
|
||||
*/
|
||||
public static function bind(BaseClosure|null $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
|
||||
{
|
||||
if ($closure == null) {
|
||||
throw ShouldNotHappen::fromMessage('Could not bind null closure.');
|
||||
}
|
||||
|
||||
$closure = BaseClosure::bind($closure, $newThis, $newScope);
|
||||
|
||||
if ($closure == false) {
|
||||
throw ShouldNotHappen::fromMessage('Could not bind closure.');
|
||||
}
|
||||
|
||||
return $closure;
|
||||
}
|
||||
}
|
||||
@ -35,15 +35,15 @@ final class Container
|
||||
/**
|
||||
* Gets a dependency from the container.
|
||||
*
|
||||
* @return object
|
||||
* @param class-string $id
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $id)
|
||||
{
|
||||
if (array_key_exists($id, $this->instances)) {
|
||||
return $this->instances[$id];
|
||||
}
|
||||
|
||||
if (!array_key_exists($id, $this->instances)) {
|
||||
$this->instances[$id] = $this->build($id);
|
||||
}
|
||||
|
||||
return $this->instances[$id];
|
||||
}
|
||||
@ -60,10 +60,11 @@ final class Container
|
||||
|
||||
/**
|
||||
* Tries to build the given instance.
|
||||
*
|
||||
* @param class-string $id
|
||||
*/
|
||||
private function build(string $id): object
|
||||
{
|
||||
/** @phpstan-ignore-next-line */
|
||||
$reflectionClass = new ReflectionClass($id);
|
||||
|
||||
if ($reflectionClass->isInstantiable()) {
|
||||
@ -84,6 +85,7 @@ final class Container
|
||||
}
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
return $this->get($candidate);
|
||||
},
|
||||
$constructor->getParameters()
|
||||
|
||||
@ -50,6 +50,8 @@ final class ExceptionTrace
|
||||
|
||||
$property = new ReflectionProperty($t, 'serializableTrace');
|
||||
$property->setAccessible(true);
|
||||
|
||||
/** @var array<int, array<string, string>> $trace */
|
||||
$trace = $property->getValue($t);
|
||||
|
||||
$cleanedTrace = [];
|
||||
|
||||
@ -14,21 +14,22 @@ final class ExpectationPipeline
|
||||
/** @var array<Closure> */
|
||||
private array $pipes = [];
|
||||
|
||||
/** @var array<int|string, mixed> */
|
||||
/** @var array<mixed> */
|
||||
private array $passable;
|
||||
|
||||
public function __construct(
|
||||
private Closure $expectationClosure
|
||||
) {
|
||||
//..
|
||||
private Closure $expectationClosure;
|
||||
|
||||
public function __construct(Closure $expectationClosure)
|
||||
{
|
||||
$this->expectationClosure = $expectationClosure;
|
||||
}
|
||||
|
||||
public static function for(Closure $expectationClosure): ExpectationPipeline
|
||||
public static function for(Closure $expectationClosure): self
|
||||
{
|
||||
return new self($expectationClosure);
|
||||
}
|
||||
|
||||
public function send(mixed ...$passable): ExpectationPipeline
|
||||
public function send(mixed ...$passable): self
|
||||
{
|
||||
$this->passable = $passable;
|
||||
|
||||
@ -38,7 +39,7 @@ final class ExpectationPipeline
|
||||
/**
|
||||
* @param array<Closure> $pipes
|
||||
*/
|
||||
public function through(array $pipes): ExpectationPipeline
|
||||
public function through(array $pipes): self
|
||||
{
|
||||
$this->pipes = $pipes;
|
||||
|
||||
@ -60,6 +61,10 @@ final class ExpectationPipeline
|
||||
|
||||
public function carry(): Closure
|
||||
{
|
||||
return fn ($stack, $pipe): Closure => fn () => $pipe($stack, ...$this->passable);
|
||||
return function ($stack, $pipe): Closure {
|
||||
return function () use ($stack, $pipe) {
|
||||
return $pipe($stack, ...$this->passable);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,15 +25,15 @@ final class Extendable
|
||||
$this->extendableClass::extend($name, $extend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register pipe to be applied to the given expectation.
|
||||
*/
|
||||
public function pipe(string $name, Closure $handler): void
|
||||
public function pipe(string $name, Closure $pipe): void
|
||||
{
|
||||
$this->extendableClass::pipe($name, $handler);
|
||||
$this->extendableClass::pipe($name, $pipe);
|
||||
}
|
||||
|
||||
public function intercept(string $name, string|Closure $filter, Closure $handler): void
|
||||
/**
|
||||
* @param string|Closure $filter
|
||||
*/
|
||||
public function intercept(string $name, $filter, Closure $handler): void
|
||||
{
|
||||
$this->extendableClass::intercept($name, $filter, $handler);
|
||||
}
|
||||
|
||||
@ -25,13 +25,16 @@ final class HigherOrderCallables
|
||||
*
|
||||
* Create a new expectation. Callable values will be executed prior to returning the new expectation.
|
||||
*
|
||||
* @param callable|TValue $value
|
||||
* @param (Closure():TValue)|TValue $value
|
||||
*
|
||||
* @return Expectation<TValue>
|
||||
*/
|
||||
public function expect(mixed $value): Expectation
|
||||
{
|
||||
return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value);
|
||||
/** @var TValue $value */
|
||||
$value = $value instanceof Closure ? Reflection::bindCallableWithData($value) : $value;
|
||||
|
||||
return new Expectation($value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,14 +18,14 @@ final class HigherOrderMessage
|
||||
/**
|
||||
* An optional condition that will determine if the message will be executed.
|
||||
*
|
||||
* @var (callable(): bool)|null
|
||||
* @var (Closure(): bool)|null
|
||||
*/
|
||||
public $condition;
|
||||
public ?Closure $condition = null;
|
||||
|
||||
/**
|
||||
* Creates a new higher order message.
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __construct(
|
||||
public string $filename,
|
||||
@ -41,7 +41,6 @@ final class HigherOrderMessage
|
||||
*/
|
||||
public function call(object $target): mixed
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) {
|
||||
return $target;
|
||||
}
|
||||
@ -54,8 +53,7 @@ final class HigherOrderMessage
|
||||
try {
|
||||
return is_array($this->arguments)
|
||||
? Reflection::call($target, $this->name, $this->arguments)
|
||||
: $target->{$this->name};
|
||||
/* @phpstan-ignore-line */
|
||||
: $target->{$this->name}; /* @phpstan-ignore-line */
|
||||
} catch (Throwable $throwable) {
|
||||
Reflection::setPropertyValue($throwable, 'file', $this->filename);
|
||||
Reflection::setPropertyValue($throwable, 'line', $this->line);
|
||||
@ -79,7 +77,7 @@ final class HigherOrderMessage
|
||||
*/
|
||||
public function when(callable $condition): self
|
||||
{
|
||||
$this->condition = $condition;
|
||||
$this->condition = Closure::fromCallable($condition);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ final class HigherOrderMessageCollection
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function add(string $filename, int $line, string $name, array $arguments = null): void
|
||||
public function add(string $filename, int $line, string $name, ?array $arguments): void
|
||||
{
|
||||
$this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments);
|
||||
}
|
||||
@ -29,7 +29,7 @@ final class HigherOrderMessageCollection
|
||||
*
|
||||
* @param array<int, mixed>|null $arguments
|
||||
*/
|
||||
public function addWhen(callable $condition, string $filename, int $line, string $name, array $arguments = null): void
|
||||
public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void
|
||||
{
|
||||
$this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition);
|
||||
}
|
||||
@ -40,6 +40,7 @@ final class HigherOrderMessageCollection
|
||||
public function chain(object $target): void
|
||||
{
|
||||
foreach ($this->messages as $message) {
|
||||
//@phpstan-ignore-next-line
|
||||
$target = $message->call($target) ?? $target;
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ use Throwable;
|
||||
*/
|
||||
final class HigherOrderTapProxy
|
||||
{
|
||||
private const UNDEFINED_PROPERTY = 'Undefined property: P\\';
|
||||
private const UNDEFINED_PROPERTY = 'Undefined property: P\\'; // @phpstan-ignore-line
|
||||
|
||||
/**
|
||||
* Create a new tap proxy instance.
|
||||
@ -31,8 +31,7 @@ final class HigherOrderTapProxy
|
||||
*/
|
||||
public function __set(string $property, $value): void
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
$this->target->{$property} = $value;
|
||||
$this->target->{$property} = $value; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,9 +42,8 @@ final class HigherOrderTapProxy
|
||||
public function __get(string $property)
|
||||
{
|
||||
try {
|
||||
// @phpstan-ignore-next-line
|
||||
return $this->target->{$property};
|
||||
} catch (Throwable $throwable) {
|
||||
return $this->target->{$property}; // @phpstan-ignore-line
|
||||
} catch (Throwable $throwable) { // @phpstan-ignore-line
|
||||
Reflection::setPropertyValue($throwable, 'file', Backtrace::file());
|
||||
Reflection::setPropertyValue($throwable, 'line', Backtrace::line());
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +177,17 @@
|
||||
PASS Tests\Features\Expect\not
|
||||
✓ not property calls
|
||||
|
||||
PASS Tests\Features\Expect\pipe
|
||||
✓ pipe is applied and can stop pipeline
|
||||
✓ interceptor works with negated expectation
|
||||
✓ pipe works with negated expectation
|
||||
✓ pipe is run and can let the pipeline keep going
|
||||
✓ intercept is applied
|
||||
✓ intercept stops the pipeline
|
||||
✓ interception is called only when filter is met
|
||||
✓ intercept can be filtered with a closure
|
||||
✓ intercept can add new parameters to the expectation
|
||||
|
||||
PASS Tests\Features\Expect\ray
|
||||
✓ ray calls do not fail when ray is not installed
|
||||
|
||||
@ -720,5 +731,5 @@
|
||||
✓ it is a test
|
||||
✓ it uses correct parent class
|
||||
|
||||
Tests: 4 incompleted, 9 skipped, 478 passed
|
||||
Tests: 4 incompleted, 9 skipped, 487 passed
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
240
tests/Features/Expect/pipe.php
Normal file
240
tests/Features/Expect/pipe.php
Normal file
@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
use function PHPUnit\Framework\assertEquals;
|
||||
use function PHPUnit\Framework\assertEqualsIgnoringCase;
|
||||
use function PHPUnit\Framework\assertInstanceOf;
|
||||
use function PHPUnit\Framework\assertIsNumeric;
|
||||
use function PHPUnit\Framework\assertSame;
|
||||
|
||||
class Number
|
||||
{
|
||||
public $value;
|
||||
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
class Character
|
||||
{
|
||||
public $value;
|
||||
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
class Symbol
|
||||
{
|
||||
public $value;
|
||||
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
class State
|
||||
{
|
||||
public $runCount = [];
|
||||
public $appliedCount = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->runCount = [
|
||||
'character' => 0,
|
||||
'number' => 0,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
];
|
||||
|
||||
$this->appliedCount = [
|
||||
'character' => 0,
|
||||
'number' => 0,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$state = new State();
|
||||
|
||||
/*
|
||||
* Overrides toBe to assert two Characters are the same
|
||||
*/
|
||||
expect()->pipe('toBe', function ($next, $expected) use ($state) {
|
||||
$state->runCount['character']++;
|
||||
|
||||
if ($this->value instanceof Character) {
|
||||
$state->appliedCount['character']++;
|
||||
assertInstanceOf(Character::class, $expected);
|
||||
assertEquals($this->value->value, $expected->value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$next();
|
||||
});
|
||||
|
||||
/*
|
||||
* Overrides toBe to assert two Numbers are the same
|
||||
*/
|
||||
expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
|
||||
$state->runCount['number']++;
|
||||
$state->appliedCount['number']++;
|
||||
assertEquals($this->value->value, $expected->value);
|
||||
});
|
||||
|
||||
/*
|
||||
* Overrides toBe to assert all integers are allowed if value is an '*'
|
||||
*/
|
||||
expect()->intercept('toBe', function ($value) {
|
||||
return $value === '*';
|
||||
}, function ($expected) use ($state) {
|
||||
$state->runCount['wildcard']++;
|
||||
$state->appliedCount['wildcard']++;
|
||||
assertIsNumeric($expected);
|
||||
});
|
||||
|
||||
/*
|
||||
* Overrides toBe to assert two Symbols are the same
|
||||
*/
|
||||
expect()->pipe('toBe', function ($next, $expected) use ($state) {
|
||||
$state->runCount['symbol']++;
|
||||
|
||||
if ($this->value instanceof Symbol) {
|
||||
$state->appliedCount['symbol']++;
|
||||
assertInstanceOf(Symbol::class, $expected);
|
||||
assertEquals($this->value->value, $expected->value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$next();
|
||||
});
|
||||
|
||||
/*
|
||||
* Overrides toBe check strings ignoring case
|
||||
*/
|
||||
expect()->intercept('toBe', function ($value) {
|
||||
return is_string($value);
|
||||
}, function ($expected, $ignoreCase = false) {
|
||||
if ($ignoreCase) {
|
||||
assertEqualsIgnoringCase($expected, $this->value);
|
||||
} else {
|
||||
assertSame($expected, $this->value);
|
||||
}
|
||||
});
|
||||
|
||||
test('pipe is applied and can stop pipeline', function () use ($state) {
|
||||
$letter = new Character('A');
|
||||
|
||||
$state->reset();
|
||||
|
||||
expect($letter)->toBe(new Character('A'))
|
||||
->and($state)
|
||||
->runCount->toMatchArray([
|
||||
'character' => 1,
|
||||
'number' => 0,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
])
|
||||
->appliedCount->toMatchArray([
|
||||
'character' => 1,
|
||||
'number' => 0,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
]);
|
||||
});
|
||||
|
||||
test('interceptor works with negated expectation', function () {
|
||||
$letter = new Number(1);
|
||||
|
||||
expect($letter)->not->toBe(new Character('B'));
|
||||
});
|
||||
|
||||
test('pipe works with negated expectation', function () {
|
||||
$letter = new Character('A');
|
||||
|
||||
expect($letter)->not->toBe(new Character('B'));
|
||||
});
|
||||
|
||||
test('pipe is run and can let the pipeline keep going', function () use ($state) {
|
||||
$state->reset();
|
||||
|
||||
expect(3)->toBe(3)
|
||||
->and($state)
|
||||
->runCount->toMatchArray([
|
||||
'character' => 1,
|
||||
'number' => 0,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 1,
|
||||
])
|
||||
->appliedCount->toMatchArray([
|
||||
'character' => 0,
|
||||
'number' => 0,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
]);
|
||||
});
|
||||
|
||||
test('intercept is applied', function () use ($state) {
|
||||
$number = new Number(1);
|
||||
|
||||
$state->reset();
|
||||
|
||||
expect($number)->toBe(new Number(1))
|
||||
->and($state)
|
||||
->runCount->toHaveKey('number', 1)
|
||||
->appliedCount->toHaveKey('number', 1);
|
||||
});
|
||||
|
||||
test('intercept stops the pipeline', function () use ($state) {
|
||||
$number = new Number(1);
|
||||
|
||||
$state->reset();
|
||||
|
||||
expect($number)->toBe(new Number(1))
|
||||
->and($state)
|
||||
->runCount->toMatchArray([
|
||||
'character' => 1,
|
||||
'number' => 1,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
])
|
||||
->appliedCount->toMatchArray([
|
||||
'character' => 0,
|
||||
'number' => 1,
|
||||
'wildcard' => 0,
|
||||
'symbol' => 0,
|
||||
]);
|
||||
});
|
||||
|
||||
test('interception is called only when filter is met', function () use ($state) {
|
||||
$state->reset();
|
||||
|
||||
expect(1)->toBe(1)
|
||||
->and($state)
|
||||
->runCount->toHaveKey('number', 0)
|
||||
->appliedCount->toHaveKey('number', 0);
|
||||
});
|
||||
|
||||
test('intercept can be filtered with a closure', function () use ($state) {
|
||||
$state->reset();
|
||||
|
||||
expect('*')->toBe(1)
|
||||
->and($state)
|
||||
->runCount->toHaveKey('wildcard', 1)
|
||||
->appliedCount->toHaveKey('wildcard', 1);
|
||||
});
|
||||
|
||||
test('intercept can add new parameters to the expectation', function () {
|
||||
expect('Foo')->toBe('foo', true);
|
||||
});
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -2,53 +2,55 @@
|
||||
|
||||
use Pest\Exceptions\DatasetMissing;
|
||||
use Pest\Exceptions\TestAlreadyExist;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Plugins\Environment;
|
||||
use Pest\TestSuite;
|
||||
|
||||
it('does not allow to add the same test description twice', function () {
|
||||
$testSuite = new TestSuite(getcwd(), 'tests');
|
||||
$test = function () {};
|
||||
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test));
|
||||
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test));
|
||||
$method = new TestCaseMethodFactory('foo', 'bar', null);
|
||||
|
||||
$testSuite->tests->set($method);
|
||||
$testSuite->tests->set($method);
|
||||
})->throws(
|
||||
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 () {
|
||||
$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(
|
||||
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 () {
|
||||
$testSuite = TestSuite::getInstance(getcwd(), 'tests');
|
||||
$test = function () {};
|
||||
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test));
|
||||
$testSuite->tests->set(new TestCaseFactory(__FILE__, 'bar', $test));
|
||||
|
||||
$testSuite->tests->set(new TestCaseMethodFactory('a', 'b', null));
|
||||
$testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null));
|
||||
|
||||
expect($testSuite->tests->getFilenames())->toEqual([
|
||||
__FILE__,
|
||||
__FILE__,
|
||||
'a',
|
||||
'c',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can filter the test suite filenames to those with the only method', function () {
|
||||
$testSuite = new TestSuite(getcwd(), 'tests');
|
||||
$test = function () {};
|
||||
|
||||
$testWithOnly = new TestCaseFactory(__FILE__, 'foo', $test);
|
||||
$testWithOnly = new TestCaseMethodFactory('a', 'b', null);
|
||||
$testWithOnly->only = true;
|
||||
$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([
|
||||
__FILE__,
|
||||
'a',
|
||||
]);
|
||||
});
|
||||
|
||||
@ -59,15 +61,15 @@ it('does not filter the test suite filenames to those with the only method when
|
||||
|
||||
$test = function () {};
|
||||
|
||||
$testWithOnly = new TestCaseFactory(__FILE__, 'foo', $test);
|
||||
$testWithOnly = new TestCaseMethodFactory('a', 'b', null);
|
||||
$testWithOnly->only = true;
|
||||
$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([
|
||||
__FILE__,
|
||||
'Baz/Bar/Boo.php',
|
||||
'a',
|
||||
'c',
|
||||
]);
|
||||
|
||||
Environment::name($previousEnvironment);
|
||||
|
||||
Reference in New Issue
Block a user