implemented pipe closure with $next as the first parameter

This commit is contained in:
Fabio Ivona
2021-10-10 01:02:04 +02:00
parent bc4e5b9b4e
commit fc53f08e37
6 changed files with 55 additions and 87 deletions

View File

@ -29,12 +29,17 @@ trait Extendable
static::$extends[$name] = $extend; static::$extends[$name] = $extend;
} }
/**
* Register a a pipe to be applied before an expectation is checked.
*/
public static function pipe(string $name, Closure $pipe): void public static function pipe(string $name, Closure $pipe): void
{ {
self::$pipes[$name][] = $pipe; self::$pipes[$name][] = $pipe;
} }
/** /**
* Recister an interceptor that should replace an existing expectation.
*
* @param string|Closure $filter * @param string|Closure $filter
*/ */
public static function intercept(string $name, $filter, Closure $handler): void public static function intercept(string $name, $filter, Closure $handler): void
@ -46,9 +51,7 @@ trait Extendable
} }
//@phpstan-ignore-next-line //@phpstan-ignore-next-line
self::pipe($name, function (...$arguments) use ($handler, $filter) { self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) {
$next = array_pop($arguments);
//@phpstan-ignore-next-line //@phpstan-ignore-next-line
if ($filter($this->value)) { if ($filter($this->value)) {
//@phpstan-ignore-next-line //@phpstan-ignore-next-line
@ -57,7 +60,7 @@ trait Extendable
return; return;
} }
$next(...$arguments); $next();
}); });
} }

View File

@ -1,17 +1,14 @@
<?php <?php
declare(strict_types=1);
namespace Pest\Exceptions; namespace Pest\Exceptions;
use Exception; use Exception;
class PipeException extends Exception final class PipeException extends Exception
{ {
public static function optionalParmetersShouldBecomeRequired(string $expectationName): PipeException public static function expectationNotFound(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"); return new self("Expectation $expectationName was not found in Pest");
} }

View File

@ -19,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
*/ */
@ -137,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)) {
@ -170,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
@ -273,9 +273,16 @@ final class Expectation
private function getExpectationClosure(string $name): Closure private function getExpectationClosure(string $name): Closure
{ {
if (method_exists($this->coreExpectation, $name)) { if (method_exists($this->coreExpectation, $name)) {
//@phpstan-ignore-next-line
return Closure::fromCallable([$this->coreExpectation, $name]); return Closure::fromCallable([$this->coreExpectation, $name]);
} elseif (self::hasExtend($name)) { }
return self::$extends[$name];
if (self::hasExtend($name)) {
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
if ($extend != false) {
return $extend;
}
} }
throw PipeException::expectationNotFound($name); throw PipeException::expectationNotFound($name);

View File

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace Pest\Support; namespace Pest\Support;
use Closure; use Closure;
use Pest\Exceptions\PipeException;
use ReflectionFunction;
final class ExpectationPipeline final class ExpectationPipeline
{ {
@ -25,7 +23,7 @@ final class ExpectationPipeline
public function __construct(string $expectationName, Closure $expectationClosure) public function __construct(string $expectationName, Closure $expectationClosure)
{ {
$this->expectationClosure = $expectationClosure; $this->expectationClosure = $expectationClosure;
$this->expectationName = $expectationName; $this->expectationName = $expectationName;
} }
public static function for(string $expectationName, Closure $expectationClosure): self public static function for(string $expectationName, Closure $expectationClosure): self
@ -39,6 +37,7 @@ final class ExpectationPipeline
public function send(...$passable): self public function send(...$passable): self
{ {
$this->passable = $passable; $this->passable = $passable;
return $this; return $this;
} }
@ -57,58 +56,20 @@ final class ExpectationPipeline
$pipeline = array_reduce( $pipeline = array_reduce(
array_reverse($this->pipes), array_reverse($this->pipes),
$this->carry(), $this->carry(),
$this->expectationClosure function (): void {
($this->expectationClosure)(...$this->passable);
}
); );
$pipeline(...$this->passable); $pipeline();
} }
public function carry(): Closure public function carry(): Closure
{ {
return function ($stack, $pipe): Closure { return function ($stack, $pipe): Closure {
return function (...$passable) use ($stack, $pipe) { return function () use ($stack, $pipe) {
$this->checkOptionalParametersBecomeRequired($pipe); return $pipe($stack, ...$this->passable);
$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

@ -179,6 +179,8 @@
PASS Tests\Features\Expect\pipe PASS Tests\Features\Expect\pipe
✓ pipe is applied and can stop pipeline ✓ 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 ✓ pipe is run and can let the pipeline keep going
✓ intercept is applied ✓ intercept is applied
✓ intercept stops the pipeline ✓ intercept stops the pipeline
@ -728,5 +730,5 @@
✓ it is a test ✓ it is a test
✓ it uses correct parent class ✓ it uses correct parent class
Tests: 4 incompleted, 9 skipped, 484 passed Tests: 4 incompleted, 9 skipped, 486 passed

View File

@ -1,7 +1,6 @@
<?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;
@ -37,7 +36,7 @@ class Symbol
class State class State
{ {
public $runCount = []; public $runCount = [];
public $appliedCount = []; public $appliedCount = [];
public function __construct() public function __construct()
@ -68,7 +67,7 @@ $state = new State();
/* /*
* Overrides toBe to assert 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 ($next, $expected) use ($state) {
$state->runCount['character']++; $state->runCount['character']++;
if ($this->value instanceof Character) { if ($this->value instanceof Character) {
@ -79,7 +78,7 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) {
return; return;
} }
$next($expected); $next();
}); });
/* /*
@ -105,7 +104,7 @@ expect()->intercept('toBe', function ($value) {
/* /*
* Overrides toBe to assert 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 ($next, $expected) use ($state) {
$state->runCount['symbol']++; $state->runCount['symbol']++;
if ($this->value instanceof Symbol) { if ($this->value instanceof Symbol) {
@ -116,16 +115,9 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) {
return; return;
} }
$next($expected); $next();
}); });
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');
@ -145,8 +137,19 @@ test('pipe is applied and can stop pipeline', function () use ($state) {
'wildcard' => 0, 'wildcard' => 0,
'symbol' => 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) { test('pipe is run and can let the pipeline keep going', function () use ($state) {
$state->reset(); $state->reset();
@ -216,8 +219,3 @@ 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();