mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 15:57:21 +01:00
implemented pipe closure with $next as the last parameter
This commit is contained in:
18
src/Exceptions/PipeException.php
Normal file
18
src/Exceptions/PipeException.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class PipeException extends Exception
|
||||||
|
{
|
||||||
|
public static function optionalParmetersShouldBecomeRequired(string $expectationName): PipeException
|
||||||
|
{
|
||||||
|
return new self("You're attempting to pipe '$expectationName', but in pipelines optional parmeters should be declared as required)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function expectationNotFound($expectationName): PipeException
|
||||||
|
{
|
||||||
|
return new self("Expectation $expectationName was not found in Pest");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,9 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
|
use Closure;
|
||||||
use Pest\Concerns\Extendable;
|
use Pest\Concerns\Extendable;
|
||||||
use Pest\Concerns\RetrievesValues;
|
use Pest\Concerns\RetrievesValues;
|
||||||
use Pest\Support\Pipeline;
|
use Pest\Exceptions\PipeException;
|
||||||
|
use Pest\Support\ExpectationPipeline;
|
||||||
use PHPUnit\Framework\Assert;
|
use PHPUnit\Framework\Assert;
|
||||||
use PHPUnit\Framework\ExpectationFailedException;
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ use PHPUnit\Framework\ExpectationFailedException;
|
|||||||
* @template TValue
|
* @template TValue
|
||||||
*
|
*
|
||||||
* @property Expectation $not Creates the opposite expectation.
|
* @property Expectation $not Creates the opposite expectation.
|
||||||
* @property Each $each Creates an expectation on each element on the traversable value.
|
* @property Each $each Creates an expectation on each element on the traversable value.
|
||||||
*
|
*
|
||||||
* @mixin CoreExpectation
|
* @mixin CoreExpectation
|
||||||
*/
|
*/
|
||||||
@ -135,16 +137,16 @@ final class Expectation
|
|||||||
throw new BadMethodCallException('Expectation value is not iterable.');
|
throw new BadMethodCallException('Expectation value is not iterable.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$value = is_array($this->value) ? $this->value : iterator_to_array($this->value);
|
$value = is_array($this->value) ? $this->value : iterator_to_array($this->value);
|
||||||
$keys = array_keys($value);
|
$keys = array_keys($value);
|
||||||
$values = array_values($value);
|
$values = array_values($value);
|
||||||
$callbacksCount = count($callbacks);
|
$callbacksCount = count($callbacks);
|
||||||
|
|
||||||
$index = 0;
|
$index = 0;
|
||||||
|
|
||||||
while (count($callbacks) < count($values)) {
|
while (count($callbacks) < count($values)) {
|
||||||
$callbacks[] = $callbacks[$index];
|
$callbacks[] = $callbacks[$index];
|
||||||
$index = $index < count($values) - 1 ? $index + 1 : 0;
|
$index = $index < count($values) - 1 ? $index + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($callbacksCount > count($values)) {
|
if ($callbacksCount > count($values)) {
|
||||||
@ -168,7 +170,7 @@ final class Expectation
|
|||||||
*
|
*
|
||||||
* @template TMatchSubject of array-key
|
* @template TMatchSubject of array-key
|
||||||
*
|
*
|
||||||
* @param callable(): TMatchSubject|TMatchSubject $subject
|
* @param callable(): TMatchSubject|TMatchSubject $subject
|
||||||
* @param array<TMatchSubject, (callable(Expectation<TValue>): mixed)|TValue> $expressions
|
* @param array<TMatchSubject, (callable(Expectation<TValue>): mixed)|TValue> $expressions
|
||||||
*/
|
*/
|
||||||
public function match($subject, array $expressions): Expectation
|
public function match($subject, array $expressions): Expectation
|
||||||
@ -260,28 +262,23 @@ final class Expectation
|
|||||||
return new HigherOrderExpectation($this, $this->value->$method(...$parameters));
|
return new HigherOrderExpectation($this, $this->value->$method(...$parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
Pipeline::send(...$parameters)
|
ExpectationPipeline::for($method, $this->getExpectationClosure($method))
|
||||||
|
->send(...$parameters)
|
||||||
->through($this->pipes($method, $this, Expectation::class))
|
->through($this->pipes($method, $this, Expectation::class))
|
||||||
->finally(function ($parameters) use ($method): void {
|
->run();
|
||||||
$this->callExpectation($method, $parameters);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function getExpectationClosure(string $name): Closure
|
||||||
* @param array<mixed> $parameters
|
|
||||||
*/
|
|
||||||
private function callExpectation(string $name, array $parameters): void
|
|
||||||
{
|
{
|
||||||
if (method_exists($this->coreExpectation, $name)) {
|
if (method_exists($this->coreExpectation, $name)) {
|
||||||
//@phpstan-ignore-next-line
|
return Closure::fromCallable([$this->coreExpectation, $name]);
|
||||||
$this->coreExpectation->{$name}(...$parameters);
|
} elseif (self::hasExtend($name)) {
|
||||||
} else {
|
return self::$extends[$name];
|
||||||
if (self::hasExtend($name)) {
|
|
||||||
$this->__extendsCall($name, $parameters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw PipeException::expectationNotFound($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hasExpectation(string $name): bool
|
private function hasExpectation(string $name): bool
|
||||||
|
|||||||
114
src/Support/ExpectationPipeline.php
Normal file
114
src/Support/ExpectationPipeline.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Pest\Exceptions\PipeException;
|
||||||
|
use ReflectionFunction;
|
||||||
|
|
||||||
|
final class ExpectationPipeline
|
||||||
|
{
|
||||||
|
/** @var array<Closure> */
|
||||||
|
private $pipes = [];
|
||||||
|
|
||||||
|
/** @var array<mixed> */
|
||||||
|
private $passable;
|
||||||
|
|
||||||
|
/** @var Closure */
|
||||||
|
private $expectationClosure;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private $expectationName;
|
||||||
|
|
||||||
|
public function __construct(string $expectationName, Closure $expectationClosure)
|
||||||
|
{
|
||||||
|
$this->expectationClosure = $expectationClosure;
|
||||||
|
$this->expectationName = $expectationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function for(string $expectationName, Closure $expectationClosure): self
|
||||||
|
{
|
||||||
|
return new self($expectationName, $expectationClosure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $passable
|
||||||
|
*/
|
||||||
|
public function send(...$passable): self
|
||||||
|
{
|
||||||
|
$this->passable = $passable;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<Closure> $pipes
|
||||||
|
*/
|
||||||
|
public function through(array $pipes): self
|
||||||
|
{
|
||||||
|
$this->pipes = $pipes;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$pipeline = array_reduce(
|
||||||
|
array_reverse($this->pipes),
|
||||||
|
$this->carry(),
|
||||||
|
$this->expectationClosure
|
||||||
|
);
|
||||||
|
|
||||||
|
$pipeline(...$this->passable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function carry(): Closure
|
||||||
|
{
|
||||||
|
return function ($stack, $pipe): Closure {
|
||||||
|
return function (...$passable) use ($stack, $pipe) {
|
||||||
|
$this->checkOptionalParametersBecomeRequired($pipe);
|
||||||
|
|
||||||
|
$passable = $this->preparePassable($passable);
|
||||||
|
|
||||||
|
$passable[] = $stack;
|
||||||
|
|
||||||
|
return $pipe(...$passable);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function preparePassable(array $passable): array
|
||||||
|
{
|
||||||
|
$reflection = new ReflectionFunction($this->expectationClosure);
|
||||||
|
|
||||||
|
$requiredParametersCount = $reflection->getNumberOfParameters();
|
||||||
|
|
||||||
|
|
||||||
|
if (count($passable) < $requiredParametersCount) {
|
||||||
|
foreach ($reflection->getParameters() as $index => $parameter) {
|
||||||
|
if (!isset($passable[$index])) {
|
||||||
|
$passable[$index] = $parameter->getDefaultValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkOptionalParametersBecomeRequired($pipe)
|
||||||
|
{
|
||||||
|
$reflection = new ReflectionFunction($pipe);
|
||||||
|
|
||||||
|
foreach ($reflection->getParameters() as $parameter) {
|
||||||
|
if ($parameter->isDefaultValueAvailable()) {
|
||||||
|
/*
|
||||||
|
* TODO add pipeline blame in the exception message and a stronger clarification like
|
||||||
|
* “You’re attempting to pipe ‘toBe’, but haven’t added the $actual parameter to your pipe handler”
|
||||||
|
*/
|
||||||
|
throw PipeException::optionalParmetersShouldBecomeRequired($this->expectationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,71 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Support;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
|
|
||||||
final class Pipeline
|
|
||||||
{
|
|
||||||
/** @var array<Closure> */
|
|
||||||
private $pipes = [];
|
|
||||||
|
|
||||||
/** @var array<mixed> */
|
|
||||||
private $passable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<mixed> $passable
|
|
||||||
*/
|
|
||||||
public function __construct(...$passable)
|
|
||||||
{
|
|
||||||
$this->passable = $passable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<mixed> $passable
|
|
||||||
*/
|
|
||||||
public static function send(...$passable): self
|
|
||||||
{
|
|
||||||
return new self(...$passable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<Closure> $pipes
|
|
||||||
*/
|
|
||||||
public function through(array $pipes): self
|
|
||||||
{
|
|
||||||
$this->pipes = $pipes;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function finally(Closure $finalClosure): void
|
|
||||||
{
|
|
||||||
$pipeline = array_reduce(
|
|
||||||
array_reverse($this->pipes),
|
|
||||||
$this->carry(),
|
|
||||||
$this->prepareFinalClosure($finalClosure)
|
|
||||||
);
|
|
||||||
|
|
||||||
$pipeline(...$this->passable);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function carry(): Closure
|
|
||||||
{
|
|
||||||
return function ($stack, $pipe): Closure {
|
|
||||||
return function (...$passable) use ($stack, $pipe) {
|
|
||||||
$passable[] = $stack;
|
|
||||||
|
|
||||||
return $pipe(...$passable);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function prepareFinalClosure(Closure $finalClosure): Closure
|
|
||||||
{
|
|
||||||
return function (...$passable) use ($finalClosure) {
|
|
||||||
return $finalClosure($passable);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use function PHPUnit\Framework\assertEquals;
|
use function PHPUnit\Framework\assertEquals;
|
||||||
|
use function PHPUnit\Framework\assertEqualsIgnoringCase;
|
||||||
use function PHPUnit\Framework\assertInstanceOf;
|
use function PHPUnit\Framework\assertInstanceOf;
|
||||||
use function PHPUnit\Framework\assertIsNumeric;
|
use function PHPUnit\Framework\assertIsNumeric;
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ class Symbol
|
|||||||
|
|
||||||
class State
|
class State
|
||||||
{
|
{
|
||||||
public $runCount = [];
|
public $runCount = [];
|
||||||
public $appliedCount = [];
|
public $appliedCount = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@ -47,17 +48,17 @@ class State
|
|||||||
public function reset(): void
|
public function reset(): void
|
||||||
{
|
{
|
||||||
$this->runCount = [
|
$this->runCount = [
|
||||||
'character' => 0,
|
'character' => 0,
|
||||||
'number' => 0,
|
'number' => 0,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->appliedCount = [
|
$this->appliedCount = [
|
||||||
'character' => 0,
|
'character' => 0,
|
||||||
'number' => 0,
|
'number' => 0,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +66,7 @@ class State
|
|||||||
$state = new State();
|
$state = new State();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Asserts two Characters are the same
|
* Overrides toBe to assert two Characters are the same
|
||||||
*/
|
*/
|
||||||
expect()->pipe('toBe', function ($expected, $next) use ($state) {
|
expect()->pipe('toBe', function ($expected, $next) use ($state) {
|
||||||
$state->runCount['character']++;
|
$state->runCount['character']++;
|
||||||
@ -82,7 +83,7 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Asserts two Numbers are the same
|
* Overrides toBe to assert two Numbers are the same
|
||||||
*/
|
*/
|
||||||
expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
|
expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
|
||||||
$state->runCount['number']++;
|
$state->runCount['number']++;
|
||||||
@ -91,7 +92,7 @@ expect()->intercept('toBe', Number::class, function ($expected) use ($state) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Asserts all integers are allowed if value is an '*'
|
* Overrides toBe to assert all integers are allowed if value is an '*'
|
||||||
*/
|
*/
|
||||||
expect()->intercept('toBe', function ($value) {
|
expect()->intercept('toBe', function ($value) {
|
||||||
return $value === '*';
|
return $value === '*';
|
||||||
@ -102,7 +103,7 @@ expect()->intercept('toBe', function ($value) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Asserts two Symbols are the same
|
* Overrides toBe to assert two Symbols are the same
|
||||||
*/
|
*/
|
||||||
expect()->pipe('toBe', function ($expected, $next) use ($state) {
|
expect()->pipe('toBe', function ($expected, $next) use ($state) {
|
||||||
$state->runCount['symbol']++;
|
$state->runCount['symbol']++;
|
||||||
@ -118,6 +119,13 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) {
|
|||||||
$next($expected);
|
$next($expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect()->intercept('toHaveProperty', function ($value) {
|
||||||
|
return $value instanceof Symbol && $value->value == '£';
|
||||||
|
}, function (string $propertyName, $propertyValue = null) {
|
||||||
|
assertEquals("£", $this->value->value);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('pipe is applied and can stop pipeline', function () use ($state) {
|
test('pipe is applied and can stop pipeline', function () use ($state) {
|
||||||
$letter = new Character('A');
|
$letter = new Character('A');
|
||||||
|
|
||||||
@ -126,18 +134,19 @@ test('pipe is applied and can stop pipeline', function () use ($state) {
|
|||||||
expect($letter)->toBe(new Character('A'))
|
expect($letter)->toBe(new Character('A'))
|
||||||
->and($state)
|
->and($state)
|
||||||
->runCount->toMatchArray([
|
->runCount->toMatchArray([
|
||||||
'character' => 1,
|
'character' => 1,
|
||||||
'number' => 0,
|
'number' => 0,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
])
|
])
|
||||||
->appliedCount->toMatchArray([
|
->appliedCount->toMatchArray([
|
||||||
'character' => 1,
|
'character' => 1,
|
||||||
'number' => 0,
|
'number' => 0,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
]);
|
]);
|
||||||
});
|
})
|
||||||
|
;
|
||||||
|
|
||||||
test('pipe is run and can let the pipeline keep going', function () use ($state) {
|
test('pipe is run and can let the pipeline keep going', function () use ($state) {
|
||||||
$state->reset();
|
$state->reset();
|
||||||
@ -145,16 +154,16 @@ test('pipe is run and can let the pipeline keep going', function () use ($state)
|
|||||||
expect(3)->toBe(3)
|
expect(3)->toBe(3)
|
||||||
->and($state)
|
->and($state)
|
||||||
->runCount->toMatchArray([
|
->runCount->toMatchArray([
|
||||||
'character' => 1,
|
'character' => 1,
|
||||||
'number' => 0,
|
'number' => 0,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 1,
|
'symbol' => 1,
|
||||||
])
|
])
|
||||||
->appliedCount->toMatchArray([
|
->appliedCount->toMatchArray([
|
||||||
'character' => 0,
|
'character' => 0,
|
||||||
'number' => 0,
|
'number' => 0,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,16 +186,16 @@ test('intercept stops the pipeline', function () use ($state) {
|
|||||||
expect($number)->toBe(new Number(1))
|
expect($number)->toBe(new Number(1))
|
||||||
->and($state)
|
->and($state)
|
||||||
->runCount->toMatchArray([
|
->runCount->toMatchArray([
|
||||||
'character' => 1,
|
'character' => 1,
|
||||||
'number' => 1,
|
'number' => 1,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
])
|
])
|
||||||
->appliedCount->toMatchArray([
|
->appliedCount->toMatchArray([
|
||||||
'character' => 0,
|
'character' => 0,
|
||||||
'number' => 1,
|
'number' => 1,
|
||||||
'wildcard' => 0,
|
'wildcard' => 0,
|
||||||
'symbol' => 0,
|
'symbol' => 0,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,3 +216,8 @@ test('intercept can be filtered with a closure', function () use ($state) {
|
|||||||
->runCount->toHaveKey('wildcard', 1)
|
->runCount->toHaveKey('wildcard', 1)
|
||||||
->appliedCount->toHaveKey('wildcard', 1);
|
->appliedCount->toHaveKey('wildcard', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('intercept can handle default values', function(){
|
||||||
|
expect(new Symbol("£"))->toHaveProperty('value');
|
||||||
|
expect(new Symbol("£"))->toHaveProperty('value', '£');
|
||||||
|
})->only();
|
||||||
|
|||||||
Reference in New Issue
Block a user