implemented pipe closure with $next as the last parameter

This commit is contained in:
Fabio Ivona
2021-10-10 00:16:21 +02:00
parent c3a445534b
commit bc4e5b9b4e
5 changed files with 202 additions and 130 deletions

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

View File

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

View 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
* “Youre attempting to pipe toBe, but havent added the $actual parameter to your pipe handler”
*/
throw PipeException::optionalParmetersShouldBecomeRequired($this->expectationName);
}
}
}
}

View File

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

View File

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