From d802e8814843e39d07c0b78ffd02e0c7493602b6 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 7 Oct 2021 22:59:46 +0200 Subject: [PATCH 01/45] moved old Expectation in CoreExpectation.php and made Expectation.php a decorator for it --- src/CoreExpectation.php | 743 +++++++++++++++++++++++++++++ src/Expectation.php | 750 ++---------------------------- src/Factories/TestCaseFactory.php | 2 +- src/HigherOrderExpectation.php | 2 +- tests/Features/Exceptions.php | 6 - 5 files changed, 775 insertions(+), 728 deletions(-) create mode 100644 src/CoreExpectation.php diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php new file mode 100644 index 00000000..4cd576fc --- /dev/null +++ b/src/CoreExpectation.php @@ -0,0 +1,743 @@ +value = $value; + } + + /** + * Asserts that two variables have the same type and + * value. Used on objects, it asserts that two + * variables reference the same object. + * + * @param mixed $expected + */ + public function toBe($expected): CoreExpectation + { + Assert::assertSame($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is empty. + */ + public function toBeEmpty(): CoreExpectation + { + Assert::assertEmpty($this->value); + + return $this; + } + + /** + * Asserts that the value is true. + */ + public function toBeTrue(): CoreExpectation + { + Assert::assertTrue($this->value); + + return $this; + } + + /** + * Asserts that the value is truthy. + */ + public function toBeTruthy(): CoreExpectation + { + Assert::assertTrue((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is false. + */ + public function toBeFalse(): CoreExpectation + { + Assert::assertFalse($this->value); + + return $this; + } + + /** + * Asserts that the value is falsy. + */ + public function toBeFalsy(): CoreExpectation + { + Assert::assertFalse((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than $expected. + * + * @param int|float $expected + */ + public function toBeGreaterThan($expected): CoreExpectation + { + Assert::assertGreaterThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than or equal to $expected. + * + * @param int|float $expected + */ + public function toBeGreaterThanOrEqual($expected): CoreExpectation + { + Assert::assertGreaterThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than or equal to $expected. + * + * @param int|float $expected + */ + public function toBeLessThan($expected): CoreExpectation + { + Assert::assertLessThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than $expected. + * + * @param int|float $expected + */ + public function toBeLessThanOrEqual($expected): CoreExpectation + { + Assert::assertLessThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that $needle is an element of the value. + * + * @param mixed $needles + */ + public function toContain(...$needles): CoreExpectation + { + foreach ($needles as $needle) { + if (is_string($this->value)) { + Assert::assertStringContainsString($needle, $this->value); + } else { + Assert::assertContains($needle, $this->value); + } + } + + return $this; + } + + /** + * Asserts that the value starts with $expected. + */ + public function toStartWith(string $expected): CoreExpectation + { + Assert::assertStringStartsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value ends with $expected. + */ + public function toEndWith(string $expected): CoreExpectation + { + Assert::assertStringEndsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that $number matches value's Length. + */ + public function toHaveLength(int $number): CoreExpectation + { + if (is_string($this->value)) { + Assert::assertEquals($number, mb_strlen($this->value)); + + return $this; + } + + if (is_iterable($this->value)) { + return $this->toHaveCount($number); + } + + if (is_object($this->value)) { + if (method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + Assert::assertCount($number, $array); + + return $this; + } + + throw new BadMethodCallException('Expectation value length is not countable.'); + } + + /** + * Asserts that $count matches the number of elements of the value. + */ + public function toHaveCount(int $count): CoreExpectation + { + Assert::assertCount($count, $this->value); + + return $this; + } + + /** + * Asserts that the value contains the property $name. + * + * @param mixed $value + */ + public function toHaveProperty(string $name, $value = null): CoreExpectation + { + $this->toBeObject(); + + Assert::assertTrue(property_exists($this->value, $name)); + + if (func_num_args() > 1) { + /* @phpstan-ignore-next-line */ + Assert::assertEquals($value, $this->value->{$name}); + } + + return $this; + } + + /** + * Asserts that the value contains the provided properties $names. + * + * @param iterable $names + */ + public function toHaveProperties(iterable $names): CoreExpectation + { + foreach ($names as $name) { + $this->toHaveProperty($name); + } + + return $this; + } + + /** + * Asserts that two variables have the same value. + * + * @param mixed $expected + */ + public function toEqual($expected): CoreExpectation + { + Assert::assertEquals($expected, $this->value); + + return $this; + } + + /** + * Asserts that two variables have the same value. + * The contents of $expected and the $this->value are + * canonicalized before they are compared. For instance, when the two + * variables $expected and $this->value are arrays, then these arrays + * are sorted before they are compared. When $expected and $this->value + * are objects, each object is converted to an array containing all + * private, protected and public attributes. + * + * @param mixed $expected + */ + public function toEqualCanonicalizing($expected): CoreExpectation + { + Assert::assertEqualsCanonicalizing($expected, $this->value); + + return $this; + } + + /** + * Asserts that the absolute difference between the value and $expected + * is lower than $delta. + * + * @param mixed $expected + */ + public function toEqualWithDelta($expected, float $delta): CoreExpectation + { + Assert::assertEqualsWithDelta($expected, $this->value, $delta); + + return $this; + } + + /** + * Asserts that the value is one of the given values. + * + * @param iterable $values + */ + public function toBeIn(iterable $values): CoreExpectation + { + Assert::assertContains($this->value, $values); + + return $this; + } + + /** + * Asserts that the value is infinite. + */ + public function toBeInfinite(): CoreExpectation + { + Assert::assertInfinite($this->value); + + return $this; + } + + /** + * Asserts that the value is an instance of $class. + */ + public function toBeInstanceOf(string $class): CoreExpectation + { + /* @phpstan-ignore-next-line */ + Assert::assertInstanceOf($class, $this->value); + + return $this; + } + + /** + * Asserts that the value is an array. + */ + public function toBeArray(): CoreExpectation + { + Assert::assertIsArray($this->value); + + return $this; + } + + /** + * Asserts that the value is of type bool. + */ + public function toBeBool(): CoreExpectation + { + Assert::assertIsBool($this->value); + + return $this; + } + + /** + * Asserts that the value is of type callable. + */ + public function toBeCallable(): CoreExpectation + { + Assert::assertIsCallable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type float. + */ + public function toBeFloat(): CoreExpectation + { + Assert::assertIsFloat($this->value); + + return $this; + } + + /** + * Asserts that the value is of type int. + */ + public function toBeInt(): CoreExpectation + { + Assert::assertIsInt($this->value); + + return $this; + } + + /** + * Asserts that the value is of type iterable. + */ + public function toBeIterable(): CoreExpectation + { + Assert::assertIsIterable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type numeric. + */ + public function toBeNumeric(): CoreExpectation + { + Assert::assertIsNumeric($this->value); + + return $this; + } + + /** + * Asserts that the value is of type object. + */ + public function toBeObject(): CoreExpectation + { + Assert::assertIsObject($this->value); + + return $this; + } + + /** + * Asserts that the value is of type resource. + */ + public function toBeResource(): CoreExpectation + { + Assert::assertIsResource($this->value); + + return $this; + } + + /** + * Asserts that the value is of type scalar. + */ + public function toBeScalar(): CoreExpectation + { + Assert::assertIsScalar($this->value); + + return $this; + } + + /** + * Asserts that the value is of type string. + */ + public function toBeString(): CoreExpectation + { + Assert::assertIsString($this->value); + + return $this; + } + + /** + * Asserts that the value is a JSON string. + */ + public function toBeJson(): CoreExpectation + { + Assert::assertIsString($this->value); + Assert::assertJson($this->value); + + return $this; + } + + /** + * Asserts that the value is NAN. + */ + public function toBeNan(): CoreExpectation + { + Assert::assertNan($this->value); + + return $this; + } + + /** + * Asserts that the value is null. + */ + public function toBeNull(): CoreExpectation + { + Assert::assertNull($this->value); + + return $this; + } + + /** + * Asserts that the value array has the provided $key. + * + * @param string|int $key + * @param mixed $value + */ + public function toHaveKey($key, $value = null): CoreExpectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + try { + Assert::assertTrue(Arr::has($array, $key)); + + /* @phpstan-ignore-next-line */ + } catch (ExpectationFailedException $exception) { + throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); + } + + if (func_num_args() > 1) { + Assert::assertEquals($value, Arr::get($array, $key)); + } + + return $this; + } + + /** + * Asserts that the value array has the provided $keys. + * + * @param array $keys + */ + public function toHaveKeys(array $keys): CoreExpectation + { + foreach ($keys as $key) { + $this->toHaveKey($key); + } + + return $this; + } + + /** + * Asserts that the value is a directory. + */ + public function toBeDirectory(): CoreExpectation + { + Assert::assertDirectoryExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is readable. + */ + public function toBeReadableDirectory(): CoreExpectation + { + Assert::assertDirectoryIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is writable. + */ + public function toBeWritableDirectory(): CoreExpectation + { + Assert::assertDirectoryIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file. + */ + public function toBeFile(): CoreExpectation + { + Assert::assertFileExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is readable. + */ + public function toBeReadableFile(): CoreExpectation + { + Assert::assertFileIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is writable. + */ + public function toBeWritableFile(): CoreExpectation + { + Assert::assertFileIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value array matches the given array subset. + * + * @param array $array + */ + public function toMatchArray(array $array): CoreExpectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $valueAsArray = $this->value->toArray(); + } else { + $valueAsArray = (array) $this->value; + } + + foreach ($array as $key => $value) { + Assert::assertArrayHasKey($key, $valueAsArray); + + Assert::assertEquals( + $value, + $valueAsArray[$key], + sprintf( + 'Failed asserting that an array has a key %s with the value %s.', + $this->export($key), + $this->export($valueAsArray[$key]), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value object matches a subset + * of the properties of an given object. + * + * @param array|object $object + */ + public function toMatchObject($object): CoreExpectation + { + foreach ((array) $object as $property => $value) { + Assert::assertTrue(property_exists($this->value, $property)); + + /* @phpstan-ignore-next-line */ + $propertyValue = $this->value->{$property}; + Assert::assertEquals( + $value, + $propertyValue, + sprintf( + 'Failed asserting that an object has a property %s with the value %s.', + $this->export($property), + $this->export($propertyValue), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value matches a regular expression. + */ + public function toMatch(string $expression): CoreExpectation + { + Assert::assertMatchesRegularExpression($expression, $this->value); + + return $this; + } + + /** + * Asserts that the value matches a constraint. + */ + public function toMatchConstraint(Constraint $constraint): CoreExpectation + { + Assert::assertThat($this->value, $constraint); + + return $this; + } + + /** + * Asserts that executing value throws an exception. + * + * @param (Closure(Throwable): mixed)|string $exception + */ + public function toThrow($exception, string $exceptionMessage = null): CoreExpectation + { + $callback = NullClosure::create(); + + if ($exception instanceof Closure) { + $callback = $exception; + $parameters = (new ReflectionFunction($exception))->getParameters(); + + if (1 !== count($parameters)) { + throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); + } + + if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { + throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); + } + + $exception = $type->getName(); + } + + try { + ($this->value)(); + } catch (Throwable $e) { // @phpstan-ignore-line + if (!class_exists($exception)) { + Assert::assertStringContainsString($exception, $e->getMessage()); + + return $this; + } + + if ($exceptionMessage !== null) { + Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); + } + + Assert::assertInstanceOf($exception, $e); + $callback($e); + + return $this; + } + + if (!class_exists($exception)) { + throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); + } + + throw new ExpectationFailedException("Exception \"$exception\" not thrown."); + } + + /** + * Exports the given value. + * + * @param mixed $value + */ + private function export($value): string + { + if ($this->exporter === null) { + $this->exporter = new Exporter(); + } + + return $this->exporter->export($value); + } +} diff --git a/src/Expectation.php b/src/Expectation.php index a973c760..5c64b3fd 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -5,19 +5,10 @@ declare(strict_types=1); namespace Pest; use BadMethodCallException; -use Closure; -use InvalidArgumentException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; -use Pest\Support\Arr; -use Pest\Support\NullClosure; use PHPUnit\Framework\Assert; -use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; -use ReflectionFunction; -use ReflectionNamedType; -use SebastianBergmann\Exporter\Exporter; -use Throwable; /** * @internal @@ -26,6 +17,8 @@ use Throwable; * * @property Expectation $not Creates the opposite expectation. * @property Each $each Creates an expectation on each element on the traversable value. + * + * @mixin CoreExpectation */ final class Expectation { @@ -34,32 +27,17 @@ final class Expectation } use RetrievesValues; - /** - * The expectation value. - * - * @readonly - * - * @var mixed - */ - public $value; + /** @var CoreExpectation */ + private $coreExpectation; /** - * The exporter instance, if any. - * - * @readonly - * - * @var Exporter|null - */ - private $exporter; - - /** - * Creates a new expectation. + * Creates a new Expectation. * * @param TValue $value */ public function __construct($value) { - $this->value = $value; + $this->coreExpectation = new CoreExpectation($value); } /** @@ -147,6 +125,8 @@ final class Expectation * @template TSequenceValue * * @param callable(self, self): void|TSequenceValue ...$callbacks + * + * @noinspection PhpParamsInspection */ public function sequence(...$callbacks): Expectation { @@ -187,7 +167,7 @@ final class Expectation * * @template TMatchSubject of array-key * - * @param callable(): TMatchSubject|TMatchSubject $subject + * @param callable(): TMatchSubject|TMatchSubject $subject * @param array): mixed)|TValue> $expressions */ public function match($subject, array $expressions): Expectation @@ -198,7 +178,7 @@ final class Expectation return $subject; }; - $subject = $subject(); + $subject = $subject(); $matched = false; @@ -229,7 +209,7 @@ final class Expectation /** * Apply the callback if the given "condition" is falsy. * - * @param (callable(): bool)|bool $condition + * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback */ public function unless($condition, callable $callback): Expectation @@ -246,7 +226,7 @@ final class Expectation /** * Apply the callback if the given "condition" is truthy. * - * @param (callable(): bool)|bool $condition + * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback */ public function when($condition, callable $callback): Expectation @@ -264,692 +244,6 @@ final class Expectation return $this; } - /** - * Asserts that two variables have the same type and - * value. Used on objects, it asserts that two - * variables reference the same object. - * - * @param mixed $expected - */ - public function toBe($expected): Expectation - { - Assert::assertSame($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is empty. - */ - public function toBeEmpty(): Expectation - { - Assert::assertEmpty($this->value); - - return $this; - } - - /** - * Asserts that the value is true. - */ - public function toBeTrue(): Expectation - { - Assert::assertTrue($this->value); - - return $this; - } - - /** - * Asserts that the value is truthy. - */ - public function toBeTruthy(): Expectation - { - Assert::assertTrue((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is false. - */ - public function toBeFalse(): Expectation - { - Assert::assertFalse($this->value); - - return $this; - } - - /** - * Asserts that the value is falsy. - */ - public function toBeFalsy(): Expectation - { - Assert::assertFalse((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than $expected. - * - * @param int|float $expected - */ - public function toBeGreaterThan($expected): Expectation - { - Assert::assertGreaterThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than or equal to $expected. - * - * @param int|float $expected - */ - public function toBeGreaterThanOrEqual($expected): Expectation - { - Assert::assertGreaterThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than or equal to $expected. - * - * @param int|float $expected - */ - public function toBeLessThan($expected): Expectation - { - Assert::assertLessThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than $expected. - * - * @param int|float $expected - */ - public function toBeLessThanOrEqual($expected): Expectation - { - Assert::assertLessThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that $needle is an element of the value. - * - * @param mixed $needles - */ - public function toContain(...$needles): Expectation - { - foreach ($needles as $needle) { - if (is_string($this->value)) { - Assert::assertStringContainsString($needle, $this->value); - } else { - Assert::assertContains($needle, $this->value); - } - } - - return $this; - } - - /** - * Asserts that the value starts with $expected. - */ - public function toStartWith(string $expected): Expectation - { - Assert::assertStringStartsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value ends with $expected. - */ - public function toEndWith(string $expected): Expectation - { - Assert::assertStringEndsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that $number matches value's Length. - */ - public function toHaveLength(int $number): Expectation - { - if (is_string($this->value)) { - Assert::assertEquals($number, mb_strlen($this->value)); - - return $this; - } - - if (is_iterable($this->value)) { - return $this->toHaveCount($number); - } - - if (is_object($this->value)) { - if (method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - Assert::assertCount($number, $array); - - return $this; - } - - throw new BadMethodCallException('Expectation value length is not countable.'); - } - - /** - * Asserts that $count matches the number of elements of the value. - */ - public function toHaveCount(int $count): Expectation - { - Assert::assertCount($count, $this->value); - - return $this; - } - - /** - * Asserts that the value contains the property $name. - * - * @param mixed $value - */ - public function toHaveProperty(string $name, $value = null): Expectation - { - $this->toBeObject(); - - Assert::assertTrue(property_exists($this->value, $name)); - - if (func_num_args() > 1) { - /* @phpstan-ignore-next-line */ - Assert::assertEquals($value, $this->value->{$name}); - } - - return $this; - } - - /** - * Asserts that the value contains the provided properties $names. - * - * @param iterable $names - */ - public function toHaveProperties(iterable $names): Expectation - { - foreach ($names as $name) { - $this->toHaveProperty($name); - } - - return $this; - } - - /** - * Asserts that two variables have the same value. - * - * @param mixed $expected - */ - public function toEqual($expected): Expectation - { - Assert::assertEquals($expected, $this->value); - - return $this; - } - - /** - * Asserts that two variables have the same value. - * The contents of $expected and the $this->value are - * canonicalized before they are compared. For instance, when the two - * variables $expected and $this->value are arrays, then these arrays - * are sorted before they are compared. When $expected and $this->value - * are objects, each object is converted to an array containing all - * private, protected and public attributes. - * - * @param mixed $expected - */ - public function toEqualCanonicalizing($expected): Expectation - { - Assert::assertEqualsCanonicalizing($expected, $this->value); - - return $this; - } - - /** - * Asserts that the absolute difference between the value and $expected - * is lower than $delta. - * - * @param mixed $expected - */ - public function toEqualWithDelta($expected, float $delta): Expectation - { - Assert::assertEqualsWithDelta($expected, $this->value, $delta); - - return $this; - } - - /** - * Asserts that the value is one of the given values. - * - * @param iterable $values - */ - public function toBeIn(iterable $values): Expectation - { - Assert::assertContains($this->value, $values); - - return $this; - } - - /** - * Asserts that the value is infinite. - */ - public function toBeInfinite(): Expectation - { - Assert::assertInfinite($this->value); - - return $this; - } - - /** - * Asserts that the value is an instance of $class. - * - * @param string $class - */ - public function toBeInstanceOf($class): Expectation - { - /* @phpstan-ignore-next-line */ - Assert::assertInstanceOf($class, $this->value); - - return $this; - } - - /** - * Asserts that the value is an array. - */ - public function toBeArray(): Expectation - { - Assert::assertIsArray($this->value); - - return $this; - } - - /** - * Asserts that the value is of type bool. - */ - public function toBeBool(): Expectation - { - Assert::assertIsBool($this->value); - - return $this; - } - - /** - * Asserts that the value is of type callable. - */ - public function toBeCallable(): Expectation - { - Assert::assertIsCallable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type float. - */ - public function toBeFloat(): Expectation - { - Assert::assertIsFloat($this->value); - - return $this; - } - - /** - * Asserts that the value is of type int. - */ - public function toBeInt(): Expectation - { - Assert::assertIsInt($this->value); - - return $this; - } - - /** - * Asserts that the value is of type iterable. - */ - public function toBeIterable(): Expectation - { - Assert::assertIsIterable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type numeric. - */ - public function toBeNumeric(): Expectation - { - Assert::assertIsNumeric($this->value); - - return $this; - } - - /** - * Asserts that the value is of type object. - */ - public function toBeObject(): Expectation - { - Assert::assertIsObject($this->value); - - return $this; - } - - /** - * Asserts that the value is of type resource. - */ - public function toBeResource(): Expectation - { - Assert::assertIsResource($this->value); - - return $this; - } - - /** - * Asserts that the value is of type scalar. - */ - public function toBeScalar(): Expectation - { - Assert::assertIsScalar($this->value); - - return $this; - } - - /** - * Asserts that the value is of type string. - */ - public function toBeString(): Expectation - { - Assert::assertIsString($this->value); - - return $this; - } - - /** - * Asserts that the value is a JSON string. - */ - public function toBeJson(): Expectation - { - Assert::assertIsString($this->value); - Assert::assertJson($this->value); - - return $this; - } - - /** - * Asserts that the value is NAN. - */ - public function toBeNan(): Expectation - { - Assert::assertNan($this->value); - - return $this; - } - - /** - * Asserts that the value is null. - */ - public function toBeNull(): Expectation - { - Assert::assertNull($this->value); - - return $this; - } - - /** - * Asserts that the value array has the provided $key. - * - * @param string|int $key - * @param mixed $value - */ - public function toHaveKey($key, $value = null): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - try { - Assert::assertTrue(Arr::has($array, $key)); - - /* @phpstan-ignore-next-line */ - } catch (ExpectationFailedException $exception) { - throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); - } - - if (func_num_args() > 1) { - Assert::assertEquals($value, Arr::get($array, $key)); - } - - return $this; - } - - /** - * Asserts that the value array has the provided $keys. - * - * @param array $keys - */ - public function toHaveKeys(array $keys): Expectation - { - foreach ($keys as $key) { - $this->toHaveKey($key); - } - - return $this; - } - - /** - * Asserts that the value is a directory. - */ - public function toBeDirectory(): Expectation - { - Assert::assertDirectoryExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is readable. - */ - public function toBeReadableDirectory(): Expectation - { - Assert::assertDirectoryIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is writable. - */ - public function toBeWritableDirectory(): Expectation - { - Assert::assertDirectoryIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file. - */ - public function toBeFile(): Expectation - { - Assert::assertFileExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is readable. - */ - public function toBeReadableFile(): Expectation - { - Assert::assertFileIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is writable. - */ - public function toBeWritableFile(): Expectation - { - Assert::assertFileIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value array matches the given array subset. - * - * @param array $array - */ - public function toMatchArray($array): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $valueAsArray = $this->value->toArray(); - } else { - $valueAsArray = (array) $this->value; - } - - foreach ($array as $key => $value) { - Assert::assertArrayHasKey($key, $valueAsArray); - - Assert::assertEquals( - $value, - $valueAsArray[$key], - sprintf( - 'Failed asserting that an array has a key %s with the value %s.', - $this->export($key), - $this->export($valueAsArray[$key]), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value object matches a subset - * of the properties of an given object. - * - * @param array|object $object - */ - public function toMatchObject($object): Expectation - { - foreach ((array) $object as $property => $value) { - Assert::assertTrue(property_exists($this->value, $property)); - - /* @phpstan-ignore-next-line */ - $propertyValue = $this->value->{$property}; - Assert::assertEquals( - $value, - $propertyValue, - sprintf( - 'Failed asserting that an object has a property %s with the value %s.', - $this->export($property), - $this->export($propertyValue), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value matches a regular expression. - */ - public function toMatch(string $expression): Expectation - { - Assert::assertMatchesRegularExpression($expression, $this->value); - - return $this; - } - - /** - * Asserts that the value matches a constraint. - */ - public function toMatchConstraint(Constraint $constraint): Expectation - { - Assert::assertThat($this->value, $constraint); - - return $this; - } - - /** - * Asserts that executing value throws an exception. - * - * @param (Closure(Throwable): mixed)|string $exception - */ - public function toThrow($exception, string $exceptionMessage = null): Expectation - { - $callback = NullClosure::create(); - - if ($exception instanceof Closure) { - $callback = $exception; - $parameters = (new ReflectionFunction($exception))->getParameters(); - - if (1 !== count($parameters)) { - throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); - } - - if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { - throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); - } - - $exception = $type->getName(); - } - - try { - ($this->value)(); - } catch (Throwable $e) { // @phpstan-ignore-line - if (!class_exists($exception)) { - Assert::assertStringContainsString($exception, $e->getMessage()); - - return $this; - } - - if ($exceptionMessage !== null) { - Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); - } - - Assert::assertInstanceOf($exception, $e); - $callback($e); - - return $this; - } - - if (!class_exists($exception)) { - throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); - } - - throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); - } - - /** - * Exports the given value. - * - * @param mixed $value - */ - private function export($value): string - { - if ($this->exporter === null) { - $this->exporter = new Exporter(); - } - - return $this->exporter->export($value); - } - /** * Dynamically handle calls to the class or * creates a new higher order expectation. @@ -960,7 +254,14 @@ final class Expectation */ public function __call(string $method, array $parameters) { - if (!static::hasExtend($method)) { + if (method_exists($this->coreExpectation, $method)) { + //@phpstan-ignore-next-line + $this->coreExpectation = $this->coreExpectation->{$method}(...$parameters); + + return $this; + } + + if (!self::hasExtend($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } @@ -976,11 +277,20 @@ final class Expectation */ public function __get(string $name) { - if (!method_exists($this, $name) && !static::hasExtend($name)) { + if ($name === 'value') { + return $this->coreExpectation->value; + } + + if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !self::hasExtend($name)) { return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } /* @phpstan-ignore-next-line */ return $this->{$name}(); } + + public static function hasMethod(string $name): bool + { + return method_exists(CoreExpectation::class, $name); + } } diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 42c3ca6e..bc75f5c1 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -144,8 +144,8 @@ final class TestCaseFactory * @return mixed */ $test = function () use ($chains, $proxies, $factoryTest) { - $chains->chain($this); $proxies->proxy($this); + $chains->chain($this); /* @phpstan-ignore-next-line */ return call_user_func(Closure::bind($factoryTest, $this, get_class($this)), ...func_get_args()); diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index e436da0c..4974c99f 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -113,7 +113,7 @@ final class HigherOrderExpectation */ private function expectationHasMethod(string $name): bool { - return method_exists($this->original, $name) || $this->original::hasExtend($name); + return method_exists($this->original, $name) || $this->original::hasMethod($name) || $this->original::hasExtend($name); } /** diff --git a/tests/Features/Exceptions.php b/tests/Features/Exceptions.php index 22d9d4ea..9970c2a9 100644 --- a/tests/Features/Exceptions.php +++ b/tests/Features/Exceptions.php @@ -1,7 +1,5 @@ expectException(InvalidArgumentException::class); @@ -39,7 +37,3 @@ it('can just define the message if given condition is true', function () { it('can just define the message if given condition is 1', function () { throw new Exception('Something bad happened'); })->throwsIf(1, 'Something bad happened'); - -it('can handle a skipped test if it is trying to catch an exception', function () { - expect(1)->toBe(2); -})->throws(ExpectationFailedException::class)->skip('this test should be skipped')->only(); From e92d9bfaae44c70ee94c7f54617725033ef9aaaa Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 8 Oct 2021 15:29:35 +0200 Subject: [PATCH 02/45] implements decorators pipeline --- src/Concerns/Extendable.php | 34 +++++++++ src/Expectation.php | 48 ++++++++++--- src/Support/Extendable.php | 5 ++ src/Support/Pipeline.php | 69 +++++++++++++++++++ .../Features/Expect/HigherOrder/decorate.php | 56 +++++++++++++++ 5 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 src/Support/Pipeline.php create mode 100644 tests/Features/Expect/HigherOrder/decorate.php diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index ee87f9ab..e5758ed1 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -17,6 +17,9 @@ trait Extendable */ private static $extends = []; + /** @var array> */ + private static $decorators = []; + /** * Register a custom extend. */ @@ -25,6 +28,11 @@ trait Extendable static::$extends[$name] = $extend; } + public static function decorate(string $name, Closure $decorator): void + { + static::$decorators[$name][] = $decorator; + } + /** * Checks if extend is registered. */ @@ -33,6 +41,32 @@ trait Extendable return array_key_exists($name, static::$extends); } + /** + * Checks if decorator are registered. + */ + public static function hasDecorators(string $name): bool + { + return array_key_exists($name, static::$decorators); + } + + /** + * @return array + */ + public function decorators(string $name, object $context, string $scope): array + { + if (!self::hasDecorators($name)) { + return []; + } + + $decorators = []; + foreach (self::$decorators[$name] as $decorator) { + //@phpstan-ignore-next-line + $decorators[] = $decorator->bindTo($context, $scope); + } + + return $decorators; + } + /** * Dynamically handle calls to the class. * diff --git a/src/Expectation.php b/src/Expectation.php index 5c64b3fd..aa6b3efc 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,6 +7,7 @@ namespace Pest; use BadMethodCallException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; +use Pest\Support\Pipeline; use PHPUnit\Framework\Assert; use PHPUnit\Framework\ExpectationFailedException; @@ -250,23 +251,50 @@ final class Expectation * * @param array $parameters * - * @return HigherOrderExpectation|mixed + * @return HigherOrderExpectation|Expectation */ public function __call(string $method, array $parameters) { - if (method_exists($this->coreExpectation, $method)) { - //@phpstan-ignore-next-line - $this->coreExpectation = $this->coreExpectation->{$method}(...$parameters); - - return $this; - } - - if (!self::hasExtend($method)) { + if (!$this->hasExpectation($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } - return $this->__extendsCall($method, $parameters); + Pipeline::send(...$parameters) + ->through($this->decorators($method, $this, Expectation::class)) + ->finally(function ($parameters) use ($method): void { + $this->callExpectation($method, $parameters); + }); + + return $this; + } + + /** + * @param array $parameters + */ + private function callExpectation(string $name, array $parameters): void + { + if (method_exists($this->coreExpectation, $name)) { + //@phpstan-ignore-next-line + $this->coreExpectation->{$name}(...$parameters); + } else { + if (self::hasExtend($name)) { + $this->__extendsCall($name, $parameters); + } + } + } + + private function hasExpectation(string $name): bool + { + if (method_exists($this->coreExpectation, $name)) { + return true; + } + + if (self::hasExtend($name)) { + return true; + } + + return false; } /** diff --git a/src/Support/Extendable.php b/src/Support/Extendable.php index dcbb7a5a..341bea02 100644 --- a/src/Support/Extendable.php +++ b/src/Support/Extendable.php @@ -30,4 +30,9 @@ final class Extendable { $this->extendableClass::extend($name, $extend); } + + public function decorate(string $name, Closure $extend): void + { + $this->extendableClass::decorate($name, $extend); + } } diff --git a/src/Support/Pipeline.php b/src/Support/Pipeline.php new file mode 100644 index 00000000..2fd7121b --- /dev/null +++ b/src/Support/Pipeline.php @@ -0,0 +1,69 @@ + */ + private $pipes = []; + + /** @var array */ + private $passable; + + /** + * @param array $passable + */ + public function __construct(...$passable) + { + $this->passable = $passable; + } + + /** + * @param array $passable + */ + public static function send(...$passable): self + { + return new self(...$passable); + } + + /** + * @param array $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) { + return $pipe($stack, ...$passable); + }; + }; + } + + private function prepareFinalClosure(Closure $finalClosure): Closure + { + return function (...$passable) use ($finalClosure) { + return $finalClosure($passable); + }; + } +} diff --git a/tests/Features/Expect/HigherOrder/decorate.php b/tests/Features/Expect/HigherOrder/decorate.php new file mode 100644 index 00000000..cb1602f7 --- /dev/null +++ b/tests/Features/Expect/HigherOrder/decorate.php @@ -0,0 +1,56 @@ +value = $value; + } +} + +class Character +{ + public $value; + + public function __construct($value) + { + $this->value = $value; + } +} + +expect()->decorate('toBe', function ($next, $expected) { + if ($this->value instanceof Character) { + assertInstanceOf(Character::class, $expected); + assertEquals($this->value->value, $expected->value); + + return; + } + + $next($expected); +}); + +expect()->decorate('toBe', function ($next, $expected) { + if ($this->value instanceof Number) { + assertInstanceOf(Number::class, $expected); + assertEquals($this->value->value, $expected->value); + + return; + } + + $next($expected); +}); + +test('pass', function () { + $number = new Number(1); + + $letter = new Character('A'); + + expect($number)->toBe(new Number(1)); + expect($letter)->toBe(new Character('A')); + expect(3)->toBe(3); +}); From ba9b06adf3b29c25192e9289f39c30e68e76b0dc Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 8 Oct 2021 16:35:31 +0200 Subject: [PATCH 03/45] fix tests --- src/Concerns/Extendable.php | 3 ++- tests/.snapshots/success.txt | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index e5758ed1..babee356 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -60,10 +60,11 @@ trait Extendable $decorators = []; foreach (self::$decorators[$name] as $decorator) { - //@phpstan-ignore-next-line + $decorators[] = $decorator->bindTo($context, $scope); } + //@phpstan-ignore-next-line return $decorators; } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 0c6a57ab..4ecdfbc9 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -113,6 +113,9 @@ ✓ it can just define the message if given condition is true ✓ it can just define the message if given condition is 1 + PASS Tests\Features\Expect\HigherOrder\decorate + ✓ pass + PASS Tests\Features\Expect\HigherOrder\methods ✓ it can access methods ✓ it can access multiple methods @@ -720,5 +723,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 478 passed + Tests: 4 incompleted, 9 skipped, 479 passed \ No newline at end of file From 88355020746f0a501a83d65d6be476a20c44518f Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 8 Oct 2021 16:38:16 +0200 Subject: [PATCH 04/45] lint --- src/Concerns/Extendable.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index babee356..860f2c4e 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -60,7 +60,6 @@ trait Extendable $decorators = []; foreach (self::$decorators[$name] as $decorator) { - $decorators[] = $decorator->bindTo($context, $scope); } From 55376d32e54ef1d3a3f9016e4cfd97045100544c Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sat, 9 Oct 2021 10:22:24 +0200 Subject: [PATCH 05/45] moved decorate implementation to dedicated intercept and pipe calls --- src/Concerns/Extendable.php | 46 +++++++++++++++---- src/Expectation.php | 2 +- src/Support/Extendable.php | 12 ++++- src/Support/Pipeline.php | 4 +- .../{HigherOrder/decorate.php => pipe.php} | 13 ++---- 5 files changed, 54 insertions(+), 23 deletions(-) rename tests/Features/Expect/{HigherOrder/decorate.php => pipe.php} (71%) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 860f2c4e..389f0cf9 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -6,6 +6,7 @@ namespace Pest\Concerns; use BadMethodCallException; use Closure; +use Pest\Expectation; /** * @internal @@ -18,7 +19,7 @@ trait Extendable private static $extends = []; /** @var array> */ - private static $decorators = []; + private static $pipes = []; /** * Register a custom extend. @@ -28,9 +29,36 @@ trait Extendable static::$extends[$name] = $extend; } - public static function decorate(string $name, Closure $decorator): void + public static function pipe(string $name, Closure $pipe): void { - static::$decorators[$name][] = $decorator; + self::$pipes[$name][] = $pipe; + } + + /** + * @param string|Closure $filter + */ + public static function intercept(string $name, $filter, Closure $handler): void + { + if (is_string($filter)) { + $filter = function ($value) use ($filter): bool { + return $value instanceof $filter; + }; + } + + //@phpstan-ignore-next-line + self::pipe($name, function (...$arguments) use ($handler, $filter) { + $next = array_pop($arguments); + + //@phpstan-ignore-next-line + if ($filter($this->value)) { + //@phpstan-ignore-next-line + $handler->bindTo($this, get_class($this))(...$arguments); + + return; + } + + $next(...$arguments); + }); } /** @@ -42,24 +70,24 @@ trait Extendable } /** - * Checks if decorator are registered. + * Checks if pipes are registered for a given expectation. */ - public static function hasDecorators(string $name): bool + public static function hasPipes(string $name): bool { - return array_key_exists($name, static::$decorators); + return array_key_exists($name, static::$pipes); } /** * @return array */ - public function decorators(string $name, object $context, string $scope): array + public function pipes(string $name, object $context, string $scope): array { - if (!self::hasDecorators($name)) { + if (!self::hasPipes($name)) { return []; } $decorators = []; - foreach (self::$decorators[$name] as $decorator) { + foreach (self::$pipes[$name] as $decorator) { $decorators[] = $decorator->bindTo($context, $scope); } diff --git a/src/Expectation.php b/src/Expectation.php index aa6b3efc..fbd8af2d 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -261,7 +261,7 @@ final class Expectation } Pipeline::send(...$parameters) - ->through($this->decorators($method, $this, Expectation::class)) + ->through($this->pipes($method, $this, Expectation::class)) ->finally(function ($parameters) use ($method): void { $this->callExpectation($method, $parameters); }); diff --git a/src/Support/Extendable.php b/src/Support/Extendable.php index 341bea02..e62c0e5d 100644 --- a/src/Support/Extendable.php +++ b/src/Support/Extendable.php @@ -31,8 +31,16 @@ final class Extendable $this->extendableClass::extend($name, $extend); } - public function decorate(string $name, Closure $extend): void + public function pipe(string $name, Closure $pipe): void { - $this->extendableClass::decorate($name, $extend); + $this->extendableClass::pipe($name, $pipe); + } + + /** + * @param string|Closure $filter + */ + public function intercept(string $name, $filter, Closure $handler): void + { + $this->extendableClass::intercept($name, $filter, $handler); } } diff --git a/src/Support/Pipeline.php b/src/Support/Pipeline.php index 2fd7121b..4501f278 100644 --- a/src/Support/Pipeline.php +++ b/src/Support/Pipeline.php @@ -55,7 +55,9 @@ final class Pipeline { return function ($stack, $pipe): Closure { return function (...$passable) use ($stack, $pipe) { - return $pipe($stack, ...$passable); + $passable[] = $stack; + + return $pipe(...$passable); }; }; } diff --git a/tests/Features/Expect/HigherOrder/decorate.php b/tests/Features/Expect/pipe.php similarity index 71% rename from tests/Features/Expect/HigherOrder/decorate.php rename to tests/Features/Expect/pipe.php index cb1602f7..e2c6ac98 100644 --- a/tests/Features/Expect/HigherOrder/decorate.php +++ b/tests/Features/Expect/pipe.php @@ -23,7 +23,7 @@ class Character } } -expect()->decorate('toBe', function ($next, $expected) { +expect()->pipe('toBe', function ($expected, $next) { if ($this->value instanceof Character) { assertInstanceOf(Character::class, $expected); assertEquals($this->value->value, $expected->value); @@ -34,15 +34,8 @@ expect()->decorate('toBe', function ($next, $expected) { $next($expected); }); -expect()->decorate('toBe', function ($next, $expected) { - if ($this->value instanceof Number) { - assertInstanceOf(Number::class, $expected); - assertEquals($this->value->value, $expected->value); - - return; - } - - $next($expected); +expect()->intercept('toBe', Number::class, function ($expected) { + assertEquals($this->value->value, $expected->value); }); test('pass', function () { From 70877bfad44c9a25c3711c81989d3c9cbe8558a9 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sat, 9 Oct 2021 10:26:22 +0200 Subject: [PATCH 06/45] updated snapshots --- tests/.snapshots/success.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 4ecdfbc9..1818c459 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -113,9 +113,6 @@ ✓ it can just define the message if given condition is true ✓ it can just define the message if given condition is 1 - PASS Tests\Features\Expect\HigherOrder\decorate - ✓ pass - PASS Tests\Features\Expect\HigherOrder\methods ✓ it can access methods ✓ it can access multiple methods @@ -180,6 +177,9 @@ PASS Tests\Features\Expect\not ✓ not property calls + PASS Tests\Features\Expect\pipe + ✓ pass + PASS Tests\Features\Expect\ray ✓ ray calls do not fail when ray is not installed From c3a445534baa98f9840439e27aec740449077274 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sat, 9 Oct 2021 12:03:12 +0200 Subject: [PATCH 07/45] adds tests --- tests/.snapshots/success.txt | 9 +- tests/Features/Expect/pipe.php | 174 +++++++++++++++++++++++++++++++-- 2 files changed, 174 insertions(+), 9 deletions(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 1818c459..73cd151e 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -178,7 +178,12 @@ ✓ not property calls PASS Tests\Features\Expect\pipe - ✓ pass + ✓ pipe is applied and can stop pipeline + ✓ 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 PASS Tests\Features\Expect\ray ✓ ray calls do not fail when ray is not installed @@ -723,5 +728,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 479 passed + Tests: 4 incompleted, 9 skipped, 484 passed \ No newline at end of file diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php index e2c6ac98..1d864f9c 100644 --- a/tests/Features/Expect/pipe.php +++ b/tests/Features/Expect/pipe.php @@ -2,6 +2,7 @@ use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertIsNumeric; class Number { @@ -23,8 +24,54 @@ class Character } } -expect()->pipe('toBe', function ($expected, $next) { +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(); + +/* + * Asserts two Characters are the same + */ +expect()->pipe('toBe', function ($expected, $next) use ($state) { + $state->runCount['character']++; + if ($this->value instanceof Character) { + $state->appliedCount['character']++; assertInstanceOf(Character::class, $expected); assertEquals($this->value->value, $expected->value); @@ -34,16 +81,129 @@ expect()->pipe('toBe', function ($expected, $next) { $next($expected); }); -expect()->intercept('toBe', Number::class, function ($expected) { +/* + * Asserts 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); }); -test('pass', function () { - $number = new Number(1); +/* + * Asserts 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); +}); +/* + * Asserts two Symbols are the same + */ +expect()->pipe('toBe', function ($expected, $next) 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($expected); +}); + +test('pipe is applied and can stop pipeline', function () use ($state) { $letter = new Character('A'); - expect($number)->toBe(new Number(1)); - expect($letter)->toBe(new Character('A')); - expect(3)->toBe(3); + $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('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); }); From bc4e5b9b4e5e5005d45e1283495f1143dd6bdf37 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 10 Oct 2021 00:16:21 +0200 Subject: [PATCH 08/45] implemented pipe closure with $next as the last parameter --- src/Exceptions/PipeException.php | 18 +++++ src/Expectation.php | 39 +++++----- src/Support/ExpectationPipeline.php | 114 ++++++++++++++++++++++++++++ src/Support/Pipeline.php | 71 ----------------- tests/Features/Expect/pipe.php | 90 ++++++++++++---------- 5 files changed, 202 insertions(+), 130 deletions(-) create mode 100644 src/Exceptions/PipeException.php create mode 100644 src/Support/ExpectationPipeline.php delete mode 100644 src/Support/Pipeline.php diff --git a/src/Exceptions/PipeException.php b/src/Exceptions/PipeException.php new file mode 100644 index 00000000..c41e221e --- /dev/null +++ b/src/Exceptions/PipeException.php @@ -0,0 +1,18 @@ +value) ? $this->value : iterator_to_array($this->value); - $keys = array_keys($value); - $values = array_values($value); + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; - $index = $index < count($values) - 1 ? $index + 1 : 0; + $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { @@ -168,7 +170,7 @@ final class Expectation * * @template TMatchSubject of array-key * - * @param callable(): TMatchSubject|TMatchSubject $subject + * @param callable(): TMatchSubject|TMatchSubject $subject * @param array): mixed)|TValue> $expressions */ public function match($subject, array $expressions): Expectation @@ -260,28 +262,23 @@ final class Expectation 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)) - ->finally(function ($parameters) use ($method): void { - $this->callExpectation($method, $parameters); - }); + ->run(); return $this; } - /** - * @param array $parameters - */ - private function callExpectation(string $name, array $parameters): void + private function getExpectationClosure(string $name): Closure { if (method_exists($this->coreExpectation, $name)) { - //@phpstan-ignore-next-line - $this->coreExpectation->{$name}(...$parameters); - } else { - if (self::hasExtend($name)) { - $this->__extendsCall($name, $parameters); - } + return Closure::fromCallable([$this->coreExpectation, $name]); + } elseif (self::hasExtend($name)) { + return self::$extends[$name]; } + + throw PipeException::expectationNotFound($name); } private function hasExpectation(string $name): bool diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php new file mode 100644 index 00000000..bef4bdc6 --- /dev/null +++ b/src/Support/ExpectationPipeline.php @@ -0,0 +1,114 @@ + */ + private $pipes = []; + + /** @var array */ + 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 $passable + */ + public function send(...$passable): self + { + $this->passable = $passable; + return $this; + } + + /** + * @param array $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); + } + } + } +} diff --git a/src/Support/Pipeline.php b/src/Support/Pipeline.php deleted file mode 100644 index 4501f278..00000000 --- a/src/Support/Pipeline.php +++ /dev/null @@ -1,71 +0,0 @@ - */ - private $pipes = []; - - /** @var array */ - private $passable; - - /** - * @param array $passable - */ - public function __construct(...$passable) - { - $this->passable = $passable; - } - - /** - * @param array $passable - */ - public static function send(...$passable): self - { - return new self(...$passable); - } - - /** - * @param array $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); - }; - } -} diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php index 1d864f9c..d21ea5ed 100644 --- a/tests/Features/Expect/pipe.php +++ b/tests/Features/Expect/pipe.php @@ -1,6 +1,7 @@ runCount = [ - 'character' => 0, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]; $this->appliedCount = [ - 'character' => 0, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]; } } @@ -65,7 +66,7 @@ class 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) { $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) { $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) { 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) { $state->runCount['symbol']++; @@ -118,6 +119,13 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { $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) { $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')) ->and($state) ->runCount->toMatchArray([ - 'character' => 1, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]) ->appliedCount->toMatchArray([ - 'character' => 1, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]); -}); +}) +; test('pipe is run and can let the pipeline keep going', function () use ($state) { $state->reset(); @@ -145,16 +154,16 @@ test('pipe is run and can let the pipeline keep going', function () use ($state) expect(3)->toBe(3) ->and($state) ->runCount->toMatchArray([ - 'character' => 1, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 1, + 'character' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 1, ]) ->appliedCount->toMatchArray([ - 'character' => 0, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]); }); @@ -177,16 +186,16 @@ test('intercept stops the pipeline', function () use ($state) { expect($number)->toBe(new Number(1)) ->and($state) ->runCount->toMatchArray([ - 'character' => 1, - 'number' => 1, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 1, + 'number' => 1, + 'wildcard' => 0, + 'symbol' => 0, ]) ->appliedCount->toMatchArray([ - 'character' => 0, - 'number' => 1, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 1, + 'wildcard' => 0, + 'symbol' => 0, ]); }); @@ -207,3 +216,8 @@ test('intercept can be filtered with a closure', function () use ($state) { ->runCount->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(); From fc53f08e37b91100a9d899149e760295a4fe7bb1 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 10 Oct 2021 01:02:04 +0200 Subject: [PATCH 09/45] implemented pipe closure with $next as the first parameter --- src/Concerns/Extendable.php | 11 +++--- src/Exceptions/PipeException.php | 11 +++--- src/Expectation.php | 23 +++++++----- src/Support/ExpectationPipeline.php | 55 +++++------------------------ tests/.snapshots/success.txt | 4 ++- tests/Features/Expect/pipe.php | 38 ++++++++++---------- 6 files changed, 55 insertions(+), 87 deletions(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 389f0cf9..ce089b2d 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -29,12 +29,17 @@ trait Extendable 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 { self::$pipes[$name][] = $pipe; } /** + * Recister an interceptor that should replace an existing expectation. + * * @param string|Closure $filter */ public static function intercept(string $name, $filter, Closure $handler): void @@ -46,9 +51,7 @@ trait Extendable } //@phpstan-ignore-next-line - self::pipe($name, function (...$arguments) use ($handler, $filter) { - $next = array_pop($arguments); - + self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) { //@phpstan-ignore-next-line if ($filter($this->value)) { //@phpstan-ignore-next-line @@ -57,7 +60,7 @@ trait Extendable return; } - $next(...$arguments); + $next(); }); } diff --git a/src/Exceptions/PipeException.php b/src/Exceptions/PipeException.php index c41e221e..9d6c2a27 100644 --- a/src/Exceptions/PipeException.php +++ b/src/Exceptions/PipeException.php @@ -1,17 +1,14 @@ value) ? $this->value : iterator_to_array($this->value); - $keys = array_keys($value); - $values = array_values($value); + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; - $index = $index < count($values) - 1 ? $index + 1 : 0; + $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { @@ -170,7 +170,7 @@ final class Expectation * * @template TMatchSubject of array-key * - * @param callable(): TMatchSubject|TMatchSubject $subject + * @param callable(): TMatchSubject|TMatchSubject $subject * @param array): mixed)|TValue> $expressions */ public function match($subject, array $expressions): Expectation @@ -273,9 +273,16 @@ final class Expectation private function getExpectationClosure(string $name): Closure { if (method_exists($this->coreExpectation, $name)) { + //@phpstan-ignore-next-line 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); diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php index bef4bdc6..9ebba221 100644 --- a/src/Support/ExpectationPipeline.php +++ b/src/Support/ExpectationPipeline.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace Pest\Support; use Closure; -use Pest\Exceptions\PipeException; -use ReflectionFunction; final class ExpectationPipeline { @@ -25,7 +23,7 @@ final class ExpectationPipeline public function __construct(string $expectationName, Closure $expectationClosure) { $this->expectationClosure = $expectationClosure; - $this->expectationName = $expectationName; + $this->expectationName = $expectationName; } public static function for(string $expectationName, Closure $expectationClosure): self @@ -39,6 +37,7 @@ final class ExpectationPipeline public function send(...$passable): self { $this->passable = $passable; + return $this; } @@ -57,58 +56,20 @@ final class ExpectationPipeline $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), - $this->expectationClosure + function (): void { + ($this->expectationClosure)(...$this->passable); + } ); - $pipeline(...$this->passable); + $pipeline(); } 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); + return function () use ($stack, $pipe) { + return $pipe($stack, ...$this->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); - } - } - } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 73cd151e..eb3c3000 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -179,6 +179,8 @@ 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 @@ -728,5 +730,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 484 passed + Tests: 4 incompleted, 9 skipped, 486 passed \ No newline at end of file diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php index d21ea5ed..6234a502 100644 --- a/tests/Features/Expect/pipe.php +++ b/tests/Features/Expect/pipe.php @@ -1,7 +1,6 @@ pipe('toBe', function ($expected, $next) use ($state) { +expect()->pipe('toBe', function ($next, $expected) use ($state) { $state->runCount['character']++; if ($this->value instanceof Character) { @@ -79,7 +78,7 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { return; } - $next($expected); + $next(); }); /* @@ -105,7 +104,7 @@ expect()->intercept('toBe', function ($value) { /* * 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']++; if ($this->value instanceof Symbol) { @@ -116,16 +115,9 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { 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) { $letter = new Character('A'); @@ -145,8 +137,19 @@ test('pipe is applied and can stop pipeline', function () use ($state) { '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(); @@ -216,8 +219,3 @@ test('intercept can be filtered with a closure', function () use ($state) { ->runCount->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(); From 0346450a513c6c670aff48a7065860aa1757acd2 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 10 Oct 2021 01:09:45 +0200 Subject: [PATCH 10/45] adds test to check if pipes can add parameters to an expectation --- tests/.snapshots/success.txt | 3 ++- tests/Features/Expect/pipe.php | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index eb3c3000..e8990cbb 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -186,6 +186,7 @@ ✓ 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 @@ -730,5 +731,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 486 passed + Tests: 4 incompleted, 9 skipped, 487 passed \ No newline at end of file diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php index 6234a502..e8574acf 100644 --- a/tests/Features/Expect/pipe.php +++ b/tests/Features/Expect/pipe.php @@ -1,8 +1,10 @@ pipe('toBe', function ($next, $expected) use ($state) { $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'); @@ -219,3 +234,7 @@ test('intercept can be filtered with a closure', function () use ($state) { ->runCount->toHaveKey('wildcard', 1) ->appliedCount->toHaveKey('wildcard', 1); }); + +test('intercept can add new parameters to the expectation', function () { + expect('Foo')->toBe('foo', true); +}); From eed3ed55138cb3d2e6de9bf81c8e1784315ef653 Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Tue, 19 Oct 2021 21:40:40 +0100 Subject: [PATCH 11/45] Vastly improves the logic around bound datasets to make them more user friendly. --- src/Concerns/Testable.php | 29 +++++++++++++++++++++---- src/Support/Reflection.php | 8 +++++++ tests/.snapshots/success.txt | 13 ++++++++++-- tests/Features/Datasets.php | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 72bf02bb..a6633612 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -7,8 +7,10 @@ namespace Pest\Concerns; use Closure; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; +use Pest\Support\Reflection; use Pest\TestSuite; use PHPUnit\Framework\ExecutionOrderDependency; +use function sprintf; use Throwable; /** @@ -257,7 +259,7 @@ trait Testable */ public function toString(): string { - return \sprintf( + return sprintf( '%s::%s', self::$__filename, $this->__description @@ -283,9 +285,28 @@ trait Testable */ private function resolveTestArguments(array $arguments): array { - return array_map(function ($data) { - return $data instanceof Closure ? $this->__callClosure($data, []) : $data; - }, $arguments); + if (count($arguments) !== 1) { + return $arguments; + } + + if (!$arguments[0] instanceof Closure) { + return $arguments; + } + + $underlyingTest = Reflection::getFunctionVariable($this->__test, 'factoryTest'); + $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); + + if (in_array($testParameterTypes[0], ['Closure', 'callable'])) { + return $arguments; + } + + $boundDatasetResult = $this->__callClosure($arguments[0], []); + + if (count($testParameterTypes) === 1 || !is_array($boundDatasetResult)) { + return [$boundDatasetResult]; + } + + return array_values($boundDatasetResult); } /** diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 44cd754c..afdc1b94 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -204,4 +204,12 @@ final class Reflection return $arguments; } + + /** + * @return mixed + */ + public static function getFunctionVariable(Closure $function, string $key) + { + return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null; + } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 0c6a57ab..7bac8198 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -96,11 +96,20 @@ ✓ more than two datasets with (2) / (4) / (5) ✓ more than two datasets with (2) / (4) / (6) ✓ more than two datasets did the job right - ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) + ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #1 + ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #2 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #1 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #2 ✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #1 ✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #2 + ✓ it resolves a potential bound dataset logically with ('foo', Closure Object (...)) + ✓ it resolves a potential bound dataset logically even when the closure comes first with (Closure Object (...), 'bar') + ✓ it will not resolve a closure if it is type hinted as a closure with (Closure Object (...)) #1 + ✓ it will not resolve a closure if it is type hinted as a closure with (Closure Object (...)) #2 + ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #1 + ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2 + ✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...)) + ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...)) PASS Tests\Features\Exceptions ✓ it gives access the the underlying expectException @@ -720,5 +729,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 478 passed + Tests: 4 incompleted, 9 skipped, 487 passed \ No newline at end of file diff --git a/tests/Features/Datasets.php b/tests/Features/Datasets.php index be94e596..b65f3680 100644 --- a/tests/Features/Datasets.php +++ b/tests/Features/Datasets.php @@ -232,6 +232,7 @@ it('can resolve a dataset after the test case is available', function ($result) expect($result)->toBe('bar'); })->with([ function () { return $this->foo; }, + [function () { return $this->foo; }], ]); it('can resolve a dataset after the test case is available with shared yield sets', function ($result) { @@ -241,3 +242,43 @@ it('can resolve a dataset after the test case is available with shared yield set it('can resolve a dataset after the test case is available with shared array sets', function ($result) { expect($result)->toBeInt()->toBeLessThan(3); })->with('bound.array'); + +it('resolves a potential bound dataset logically', function ($foo, $bar) { + expect($foo)->toBe('foo'); + expect($bar())->toBe('bar'); +})->with([ + ['foo', function () { return 'bar'; }], // This should be passed as a closure because we've passed multiple arguments +]); + +it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar) { + expect($foo())->toBe('foo'); + expect($bar)->toBe('bar'); +})->with([ + [function () { return 'foo'; }, 'bar'], // This should be passed as a closure because we've passed multiple arguments +]); + +it('will not resolve a closure if it is type hinted as a closure', function (Closure $data) { + expect($data())->toBeString(); +})->with([ + function () { return 'foo'; }, + function () { return 'bar'; }, +]); + +it('will not resolve a closure if it is type hinted as a callable', function (callable $data) { + expect($data())->toBeString(); +})->with([ + function () { return 'foo'; }, + function () { return 'bar'; }, +]); + +it('can correctly resolve a bound dataset that returns an array', function (array $data) { + expect($data)->toBe(['foo', 'bar', 'baz']); +})->with([ + function () { return ['foo', 'bar', 'baz']; }, +]); + +it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz) { + expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']); +})->with([ + function () { return ['foo', 'bar', 'baz']; }, +]); From 8174f2d973a81cc6192195ad29fbf94040bd4249 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sat, 30 Oct 2021 20:14:54 +0200 Subject: [PATCH 12/45] extracted Expectations to a CoreExpectation class --- src/CoreExpectation.php | 710 +++++++++++++++++ .../ExpectationNotFoundException.php | 17 + src/Expectation.php | 747 ++---------------- src/HigherOrderExpectation.php | 2 +- 4 files changed, 787 insertions(+), 689 deletions(-) create mode 100644 src/CoreExpectation.php create mode 100644 src/Exceptions/ExpectationNotFoundException.php diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php new file mode 100644 index 00000000..dac9d6b4 --- /dev/null +++ b/src/CoreExpectation.php @@ -0,0 +1,710 @@ +value); + + return $this; + } + + /** + * Asserts that the value is empty. + */ + public function toBeEmpty(): CoreExpectation + { + Assert::assertEmpty($this->value); + + return $this; + } + + /** + * Asserts that the value is true. + */ + public function toBeTrue(): CoreExpectation + { + Assert::assertTrue($this->value); + + return $this; + } + + /** + * Asserts that the value is truthy. + */ + public function toBeTruthy(): CoreExpectation + { + Assert::assertTrue((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is false. + */ + public function toBeFalse(): CoreExpectation + { + Assert::assertFalse($this->value); + + return $this; + } + + /** + * Asserts that the value is falsy. + */ + public function toBeFalsy(): CoreExpectation + { + Assert::assertFalse((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than $expected. + */ + public function toBeGreaterThan(int|float $expected): CoreExpectation + { + Assert::assertGreaterThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than or equal to $expected. + */ + public function toBeGreaterThanOrEqual(int|float $expected): CoreExpectation + { + Assert::assertGreaterThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than or equal to $expected. + */ + public function toBeLessThan(int|float $expected): CoreExpectation + { + Assert::assertLessThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than $expected. + */ + public function toBeLessThanOrEqual(int|float $expected): CoreExpectation + { + Assert::assertLessThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that $needle is an element of the value. + */ + public function toContain(mixed ...$needles): CoreExpectation + { + foreach ($needles as $needle) { + if (is_string($this->value)) { + Assert::assertStringContainsString($needle, $this->value); + } else { + Assert::assertContains($needle, $this->value); + } + } + + return $this; + } + + /** + * Asserts that the value starts with $expected. + */ + public function toStartWith(string $expected): CoreExpectation + { + Assert::assertStringStartsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value ends with $expected. + */ + public function toEndWith(string $expected): CoreExpectation + { + Assert::assertStringEndsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that $number matches value's Length. + */ + public function toHaveLength(int $number): CoreExpectation + { + if (is_string($this->value)) { + Assert::assertEquals($number, mb_strlen($this->value)); + + return $this; + } + + if (is_iterable($this->value)) { + return $this->toHaveCount($number); + } + + if (is_object($this->value)) { + if (method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + Assert::assertCount($number, $array); + + return $this; + } + + throw new BadMethodCallException('CoreExpectation value length is not countable.'); + } + + /** + * Asserts that $count matches the number of elements of the value. + */ + public function toHaveCount(int $count): CoreExpectation + { + Assert::assertCount($count, $this->value); + + return $this; + } + + /** + * Asserts that the value contains the property $name. + */ + public function toHaveProperty(string $name, mixed $value = null): CoreExpectation + { + $this->toBeObject(); + + Assert::assertTrue(property_exists($this->value, $name)); + + if (func_num_args() > 1) { + /* @phpstan-ignore-next-line */ + Assert::assertEquals($value, $this->value->{$name}); + } + + return $this; + } + + /** + * Asserts that the value contains the provided properties $names. + * + * @param iterable $names + */ + public function toHaveProperties(iterable $names): CoreExpectation + { + foreach ($names as $name) { + $this->toHaveProperty($name); + } + + return $this; + } + + /** + * Asserts that two variables have the same value. + */ + public function toEqual(mixed $expected): CoreExpectation + { + Assert::assertEquals($expected, $this->value); + + return $this; + } + + /** + * Asserts that two variables have the same value. + * The contents of $expected and the $this->value are + * canonicalized before they are compared. For instance, when the two + * variables $expected and $this->value are arrays, then these arrays + * are sorted before they are compared. When $expected and $this->value + * are objects, each object is converted to an array containing all + * private, protected and public attributes. + */ + public function toEqualCanonicalizing(mixed $expected): CoreExpectation + { + Assert::assertEqualsCanonicalizing($expected, $this->value); + + return $this; + } + + /** + * Asserts that the absolute difference between the value and $expected + * is lower than $delta. + */ + public function toEqualWithDelta(mixed $expected, float $delta): CoreExpectation + { + Assert::assertEqualsWithDelta($expected, $this->value, $delta); + + return $this; + } + + /** + * Asserts that the value is one of the given values. + * + * @param iterable $values + */ + public function toBeIn(iterable $values): CoreExpectation + { + Assert::assertContains($this->value, $values); + + return $this; + } + + /** + * Asserts that the value is infinite. + */ + public function toBeInfinite(): CoreExpectation + { + Assert::assertInfinite($this->value); + + return $this; + } + + /** + * Asserts that the value is an instance of $class. + * + * @param class-string $class + */ + public function toBeInstanceOf(string $class): CoreExpectation + { + /* @phpstan-ignore-next-line */ + Assert::assertInstanceOf($class, $this->value); + + return $this; + } + + /** + * Asserts that the value is an array. + */ + public function toBeArray(): CoreExpectation + { + Assert::assertIsArray($this->value); + + return $this; + } + + /** + * Asserts that the value is of type bool. + */ + public function toBeBool(): CoreExpectation + { + Assert::assertIsBool($this->value); + + return $this; + } + + /** + * Asserts that the value is of type callable. + */ + public function toBeCallable(): CoreExpectation + { + Assert::assertIsCallable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type float. + */ + public function toBeFloat(): CoreExpectation + { + Assert::assertIsFloat($this->value); + + return $this; + } + + /** + * Asserts that the value is of type int. + */ + public function toBeInt(): CoreExpectation + { + Assert::assertIsInt($this->value); + + return $this; + } + + /** + * Asserts that the value is of type iterable. + */ + public function toBeIterable(): CoreExpectation + { + Assert::assertIsIterable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type numeric. + */ + public function toBeNumeric(): CoreExpectation + { + Assert::assertIsNumeric($this->value); + + return $this; + } + + /** + * Asserts that the value is of type object. + */ + public function toBeObject(): CoreExpectation + { + Assert::assertIsObject($this->value); + + return $this; + } + + /** + * Asserts that the value is of type resource. + */ + public function toBeResource(): CoreExpectation + { + Assert::assertIsResource($this->value); + + return $this; + } + + /** + * Asserts that the value is of type scalar. + */ + public function toBeScalar(): CoreExpectation + { + Assert::assertIsScalar($this->value); + + return $this; + } + + /** + * Asserts that the value is of type string. + */ + public function toBeString(): CoreExpectation + { + Assert::assertIsString($this->value); + + return $this; + } + + /** + * Asserts that the value is a JSON string. + */ + public function toBeJson(): CoreExpectation + { + Assert::assertIsString($this->value); + Assert::assertJson($this->value); + + return $this; + } + + /** + * Asserts that the value is NAN. + */ + public function toBeNan(): CoreExpectation + { + Assert::assertNan($this->value); + + return $this; + } + + /** + * Asserts that the value is null. + */ + public function toBeNull(): CoreExpectation + { + Assert::assertNull($this->value); + + return $this; + } + + /** + * Asserts that the value array has the provided $key. + */ + public function toHaveKey(string|int $key, mixed $value = null): CoreExpectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + try { + Assert::assertTrue(Arr::has($array, $key)); + + /* @phpstan-ignore-next-line */ + } catch (ExpectationFailedException $exception) { + throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); + } + + if (func_num_args() > 1) { + Assert::assertEquals($value, Arr::get($array, $key)); + } + + return $this; + } + + /** + * Asserts that the value array has the provided $keys. + * + * @param array $keys + */ + public function toHaveKeys(array $keys): CoreExpectation + { + foreach ($keys as $key) { + $this->toHaveKey($key); + } + + return $this; + } + + /** + * Asserts that the value is a directory. + */ + public function toBeDirectory(): CoreExpectation + { + Assert::assertDirectoryExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is readable. + */ + public function toBeReadableDirectory(): CoreExpectation + { + Assert::assertDirectoryIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is writable. + */ + public function toBeWritableDirectory(): CoreExpectation + { + Assert::assertDirectoryIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file. + */ + public function toBeFile(): CoreExpectation + { + Assert::assertFileExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is readable. + */ + public function toBeReadableFile(): CoreExpectation + { + Assert::assertFileIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is writable. + */ + public function toBeWritableFile(): CoreExpectation + { + Assert::assertFileIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value array matches the given array subset. + * + * @param iterable $array + */ + public function toMatchArray(iterable|object $array): CoreExpectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $valueAsArray = $this->value->toArray(); + } else { + $valueAsArray = (array) $this->value; + } + + foreach ($array as $key => $value) { + Assert::assertArrayHasKey($key, $valueAsArray); + + Assert::assertEquals( + $value, + $valueAsArray[$key], + sprintf( + 'Failed asserting that an array has a key %s with the value %s.', + $this->export($key), + $this->export($valueAsArray[$key]), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value object matches a subset + * of the properties of an given object. + * + * @param iterable|object $object + */ + public function toMatchObject(iterable|object $object): CoreExpectation + { + foreach ((array) $object as $property => $value) { + Assert::assertTrue(property_exists($this->value, $property)); + + /* @phpstan-ignore-next-line */ + $propertyValue = $this->value->{$property}; + Assert::assertEquals( + $value, + $propertyValue, + sprintf( + 'Failed asserting that an object has a property %s with the value %s.', + $this->export($property), + $this->export($propertyValue), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value matches a regular expression. + */ + public function toMatch(string $expression): CoreExpectation + { + Assert::assertMatchesRegularExpression($expression, $this->value); + + return $this; + } + + /** + * Asserts that the value matches a constraint. + */ + public function toMatchConstraint(Constraint $constraint): CoreExpectation + { + Assert::assertThat($this->value, $constraint); + + return $this; + } + + /** + * Asserts that executing value throws an exception. + * + * @param (Closure(Throwable): mixed)|string $exception + */ + public function toThrow(callable|string $exception, string $exceptionMessage = null): CoreExpectation + { + $callback = NullClosure::create(); + + if ($exception instanceof Closure) { + $callback = $exception; + $parameters = (new ReflectionFunction($exception))->getParameters(); + + if (1 !== count($parameters)) { + throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); + } + + if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { + throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); + } + + $exception = $type->getName(); + } + + try { + ($this->value)(); + } catch (Throwable $e) { // @phpstan-ignore-line + if (!class_exists($exception)) { + Assert::assertStringContainsString($exception, $e->getMessage()); + + return $this; + } + + if ($exceptionMessage !== null) { + Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); + } + + Assert::assertInstanceOf($exception, $e); + $callback($e); + + return $this; + } + + if (!class_exists($exception)) { + throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); + } + + throw new ExpectationFailedException("Exception \"$exception\" not thrown."); + } + + /** + * Exports the given value. + */ + private function export(mixed $value): string + { + if ($this->exporter === null) { + $this->exporter = new Exporter(); + } + + return $this->exporter->export($value); + } +} diff --git a/src/Exceptions/ExpectationNotFoundException.php b/src/Exceptions/ExpectationNotFoundException.php new file mode 100644 index 00000000..5b9c61ec --- /dev/null +++ b/src/Exceptions/ExpectationNotFoundException.php @@ -0,0 +1,17 @@ +coreExpectation = new CoreExpectation($value); } /** @@ -143,16 +133,16 @@ final class Expectation throw new BadMethodCallException('Expectation value is not iterable.'); } - $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); - $keys = array_keys($value); - $values = array_values($value); + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; - $index = $index < count($values) - 1 ? $index + 1 : 0; + $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { @@ -185,7 +175,7 @@ final class Expectation ? $subject : fn () => $subject; - $subject = $subject(); + $subject = $subject(); $matched = false; @@ -251,667 +241,6 @@ final class Expectation return $this; } - /** - * Asserts that two variables have the same type and - * value. Used on objects, it asserts that two - * variables reference the same object. - */ - public function toBe(mixed $expected): Expectation - { - Assert::assertSame($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is empty. - */ - public function toBeEmpty(): Expectation - { - Assert::assertEmpty($this->value); - - return $this; - } - - /** - * Asserts that the value is true. - */ - public function toBeTrue(): Expectation - { - Assert::assertTrue($this->value); - - return $this; - } - - /** - * Asserts that the value is truthy. - */ - public function toBeTruthy(): Expectation - { - Assert::assertTrue((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is false. - */ - public function toBeFalse(): Expectation - { - Assert::assertFalse($this->value); - - return $this; - } - - /** - * Asserts that the value is falsy. - */ - public function toBeFalsy(): Expectation - { - Assert::assertFalse((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than $expected. - */ - public function toBeGreaterThan(int|float $expected): Expectation - { - Assert::assertGreaterThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than or equal to $expected. - */ - public function toBeGreaterThanOrEqual(int|float $expected): Expectation - { - Assert::assertGreaterThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than or equal to $expected. - */ - public function toBeLessThan(int|float $expected): Expectation - { - Assert::assertLessThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than $expected. - */ - public function toBeLessThanOrEqual(int|float $expected): Expectation - { - Assert::assertLessThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that $needle is an element of the value. - */ - public function toContain(mixed ...$needles): Expectation - { - foreach ($needles as $needle) { - if (is_string($this->value)) { - Assert::assertStringContainsString($needle, $this->value); - } else { - Assert::assertContains($needle, $this->value); - } - } - - return $this; - } - - /** - * Asserts that the value starts with $expected. - */ - public function toStartWith(string $expected): Expectation - { - Assert::assertStringStartsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value ends with $expected. - */ - public function toEndWith(string $expected): Expectation - { - Assert::assertStringEndsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that $number matches value's Length. - */ - public function toHaveLength(int $number): Expectation - { - if (is_string($this->value)) { - Assert::assertEquals($number, mb_strlen($this->value)); - - return $this; - } - - if (is_iterable($this->value)) { - return $this->toHaveCount($number); - } - - if (is_object($this->value)) { - if (method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - Assert::assertCount($number, $array); - - return $this; - } - - throw new BadMethodCallException('Expectation value length is not countable.'); - } - - /** - * Asserts that $count matches the number of elements of the value. - */ - public function toHaveCount(int $count): Expectation - { - Assert::assertCount($count, $this->value); - - return $this; - } - - /** - * Asserts that the value contains the property $name. - */ - public function toHaveProperty(string $name, mixed $value = null): Expectation - { - $this->toBeObject(); - - Assert::assertTrue(property_exists($this->value, $name)); - - if (func_num_args() > 1) { - /* @phpstan-ignore-next-line */ - Assert::assertEquals($value, $this->value->{$name}); - } - - return $this; - } - - /** - * Asserts that the value contains the provided properties $names. - * - * @param iterable $names - */ - public function toHaveProperties(iterable $names): Expectation - { - foreach ($names as $name) { - $this->toHaveProperty($name); - } - - return $this; - } - - /** - * Asserts that two variables have the same value. - */ - public function toEqual(mixed $expected): Expectation - { - Assert::assertEquals($expected, $this->value); - - return $this; - } - - /** - * Asserts that two variables have the same value. - * The contents of $expected and the $this->value are - * canonicalized before they are compared. For instance, when the two - * variables $expected and $this->value are arrays, then these arrays - * are sorted before they are compared. When $expected and $this->value - * are objects, each object is converted to an array containing all - * private, protected and public attributes. - */ - public function toEqualCanonicalizing(mixed $expected): Expectation - { - Assert::assertEqualsCanonicalizing($expected, $this->value); - - return $this; - } - - /** - * Asserts that the absolute difference between the value and $expected - * is lower than $delta. - */ - public function toEqualWithDelta(mixed $expected, float $delta): Expectation - { - Assert::assertEqualsWithDelta($expected, $this->value, $delta); - - return $this; - } - - /** - * Asserts that the value is one of the given values. - * - * @param iterable $values - */ - public function toBeIn(iterable $values): Expectation - { - Assert::assertContains($this->value, $values); - - return $this; - } - - /** - * Asserts that the value is infinite. - */ - public function toBeInfinite(): Expectation - { - Assert::assertInfinite($this->value); - - return $this; - } - - /** - * Asserts that the value is an instance of $class. - * - * @param class-string $class - */ - public function toBeInstanceOf(string $class): Expectation - { - /* @phpstan-ignore-next-line */ - Assert::assertInstanceOf($class, $this->value); - - return $this; - } - - /** - * Asserts that the value is an array. - */ - public function toBeArray(): Expectation - { - Assert::assertIsArray($this->value); - - return $this; - } - - /** - * Asserts that the value is of type bool. - */ - public function toBeBool(): Expectation - { - Assert::assertIsBool($this->value); - - return $this; - } - - /** - * Asserts that the value is of type callable. - */ - public function toBeCallable(): Expectation - { - Assert::assertIsCallable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type float. - */ - public function toBeFloat(): Expectation - { - Assert::assertIsFloat($this->value); - - return $this; - } - - /** - * Asserts that the value is of type int. - */ - public function toBeInt(): Expectation - { - Assert::assertIsInt($this->value); - - return $this; - } - - /** - * Asserts that the value is of type iterable. - */ - public function toBeIterable(): Expectation - { - Assert::assertIsIterable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type numeric. - */ - public function toBeNumeric(): Expectation - { - Assert::assertIsNumeric($this->value); - - return $this; - } - - /** - * Asserts that the value is of type object. - */ - public function toBeObject(): Expectation - { - Assert::assertIsObject($this->value); - - return $this; - } - - /** - * Asserts that the value is of type resource. - */ - public function toBeResource(): Expectation - { - Assert::assertIsResource($this->value); - - return $this; - } - - /** - * Asserts that the value is of type scalar. - */ - public function toBeScalar(): Expectation - { - Assert::assertIsScalar($this->value); - - return $this; - } - - /** - * Asserts that the value is of type string. - */ - public function toBeString(): Expectation - { - Assert::assertIsString($this->value); - - return $this; - } - - /** - * Asserts that the value is a JSON string. - */ - public function toBeJson(): Expectation - { - Assert::assertIsString($this->value); - Assert::assertJson($this->value); - - return $this; - } - - /** - * Asserts that the value is NAN. - */ - public function toBeNan(): Expectation - { - Assert::assertNan($this->value); - - return $this; - } - - /** - * Asserts that the value is null. - */ - public function toBeNull(): Expectation - { - Assert::assertNull($this->value); - - return $this; - } - - /** - * Asserts that the value array has the provided $key. - */ - public function toHaveKey(string|int $key, mixed $value = null): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - try { - Assert::assertTrue(Arr::has($array, $key)); - - /* @phpstan-ignore-next-line */ - } catch (ExpectationFailedException $exception) { - throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); - } - - if (func_num_args() > 1) { - Assert::assertEquals($value, Arr::get($array, $key)); - } - - return $this; - } - - /** - * Asserts that the value array has the provided $keys. - * - * @param array $keys - */ - public function toHaveKeys(array $keys): Expectation - { - foreach ($keys as $key) { - $this->toHaveKey($key); - } - - return $this; - } - - /** - * Asserts that the value is a directory. - */ - public function toBeDirectory(): Expectation - { - Assert::assertDirectoryExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is readable. - */ - public function toBeReadableDirectory(): Expectation - { - Assert::assertDirectoryIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is writable. - */ - public function toBeWritableDirectory(): Expectation - { - Assert::assertDirectoryIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file. - */ - public function toBeFile(): Expectation - { - Assert::assertFileExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is readable. - */ - public function toBeReadableFile(): Expectation - { - Assert::assertFileIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is writable. - */ - public function toBeWritableFile(): Expectation - { - Assert::assertFileIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value array matches the given array subset. - * - * @param iterable $array - */ - public function toMatchArray(iterable|object $array): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $valueAsArray = $this->value->toArray(); - } else { - $valueAsArray = (array) $this->value; - } - - foreach ($array as $key => $value) { - Assert::assertArrayHasKey($key, $valueAsArray); - - Assert::assertEquals( - $value, - $valueAsArray[$key], - sprintf( - 'Failed asserting that an array has a key %s with the value %s.', - $this->export($key), - $this->export($valueAsArray[$key]), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value object matches a subset - * of the properties of an given object. - * - * @param iterable|object $object - */ - public function toMatchObject(iterable|object $object): Expectation - { - foreach ((array) $object as $property => $value) { - Assert::assertTrue(property_exists($this->value, $property)); - - /* @phpstan-ignore-next-line */ - $propertyValue = $this->value->{$property}; - Assert::assertEquals( - $value, - $propertyValue, - sprintf( - 'Failed asserting that an object has a property %s with the value %s.', - $this->export($property), - $this->export($propertyValue), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value matches a regular expression. - */ - public function toMatch(string $expression): Expectation - { - Assert::assertMatchesRegularExpression($expression, $this->value); - - return $this; - } - - /** - * Asserts that the value matches a constraint. - */ - public function toMatchConstraint(Constraint $constraint): Expectation - { - Assert::assertThat($this->value, $constraint); - - return $this; - } - - /** - * Asserts that executing value throws an exception. - * - * @param (Closure(Throwable): mixed)|string $exception - */ - public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation - { - $callback = NullClosure::create(); - - if ($exception instanceof Closure) { - $callback = $exception; - $parameters = (new ReflectionFunction($exception))->getParameters(); - - if (1 !== count($parameters)) { - throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); - } - - if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { - throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); - } - - $exception = $type->getName(); - } - - try { - ($this->value)(); - } catch (Throwable $e) { // @phpstan-ignore-line - if (!class_exists($exception)) { - Assert::assertStringContainsString($exception, $e->getMessage()); - - return $this; - } - - if ($exceptionMessage !== null) { - Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); - } - - Assert::assertInstanceOf($exception, $e); - $callback($e); - - return $this; - } - - if (!class_exists($exception)) { - throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); - } - - throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); - } - - /** - * Exports the given value. - */ - private function export(mixed $value): string - { - if ($this->exporter === null) { - $this->exporter = new Exporter(); - } - - return $this->exporter->export($value); - } - /** * Dynamically handle calls to the class or * creates a new higher order expectation. @@ -922,25 +251,67 @@ final class Expectation */ public function __call(string $method, array $parameters) { - if (!static::hasExtend($method)) { + if (!$this->hasExpectation($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } - return $this->__extendsCall($method, $parameters); + $this->getExpectationClosure($method)(...$parameters); + + return $this; } /** * Dynamically calls methods on the class without any arguments * or creates a new higher order expectation. */ - public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation + public function __get(string $name): mixed { - if (!method_exists($this, $name) && !static::hasExtend($name)) { + if ($name === 'value') { + return $this->coreExpectation->value; + } + + if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !self::hasExtend($name)) { return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } /* @phpstan-ignore-next-line */ return $this->{$name}(); } + + private function hasExpectation(string $name): bool + { + if (method_exists($this->coreExpectation, $name)) { + return true; + } + + if (self::hasExtend($name)) { + return true; + } + + return false; + } + + private function getExpectationClosure(string $name): Closure + { + if (method_exists($this->coreExpectation, $name)) { + /** @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); + } + + public static function hasMethod(string $name): bool + { + return method_exists(CoreExpectation::class, $name); + } } diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index 839f3311..dc8b2d6d 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -91,7 +91,7 @@ final class HigherOrderExpectation */ private function expectationHasMethod(string $name): bool { - return method_exists($this->original, $name) || $this->original::hasExtend($name); + return method_exists($this->original, $name) || $this->original::hasMethod($name) || $this->original::hasExtend($name); } /** From 39439197093f2b7d5cb2830313a2c6ce690fd222 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 31 Oct 2021 15:23:28 +0100 Subject: [PATCH 13/45] implemements pipelines and adds tests for it --- src/Concerns/Extendable.php | 70 +++++ src/CoreExpectation.php | 5 +- .../ExpectationNotFoundException.php | 5 +- src/Expectation.php | 25 +- src/Support/ExpectationPipeline.php | 65 +++++ src/Support/Extendable.php | 13 + tests/Features/Expect/pipes.php | 255 ++++++++++++++++++ 7 files changed, 421 insertions(+), 17 deletions(-) create mode 100644 src/Support/ExpectationPipeline.php create mode 100644 tests/Features/Expect/pipes.php diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 5cf3e79d..792be193 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -6,6 +6,8 @@ namespace Pest\Concerns; use BadMethodCallException; use Closure; +use Pest\Expectation; +use PHPStan\Type\CallableType; /** * @internal @@ -19,6 +21,9 @@ trait Extendable */ private static array $extends = []; + /** @var array> */ + private static array $pipes = []; + /** * Register a new extend. */ @@ -35,6 +40,71 @@ trait Extendable return array_key_exists($name, static::$extends); } + /** + * 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. + */ + 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)) { + /** @phpstan-ignore-next-line */ + $handler = $handler->bindTo($this, get_class($this)); + + if($handler instanceof Closure){ + $handler(...$arguments); + } + + return; + } + + $next(); + }); + } + + /** + * Checks if pipes are registered for a given expectation. + */ + public static function hasPipes(string $name): bool + { + return array_key_exists($name, static::$pipes); + } + + /** + * Gets the pipes that have been registered for a given expectation and binds them to a context and a scope. + * + * @return array + */ + private function pipes(string $name, object $context, string $scope): array + { + if (!self::hasPipes($name)) { + return []; + } + + $pipes = []; + foreach (self::$pipes[$name] as $pipe) { + $pipe = $pipe->bindTo($context, $scope); + + if($pipe instanceof Closure){ + $pipes[] = $pipe; + } + } + + return $pipes; + } + /** * Dynamically handle calls to the class. * diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php index dac9d6b4..83c36348 100644 --- a/src/CoreExpectation.php +++ b/src/CoreExpectation.php @@ -170,7 +170,7 @@ final class CoreExpectation */ public function toStartWith(string $expected): CoreExpectation { - Assert::assertStringStartsWith($expected, $this->value); + Assert::assertStringStartsWith($expected, $this->value); //@phpstan-ignore-line return $this; } @@ -180,7 +180,7 @@ final class CoreExpectation */ public function toEndWith(string $expected): CoreExpectation { - Assert::assertStringEndsWith($expected, $this->value); + Assert::assertStringEndsWith($expected, $this->value); //@phpstan-ignore-line return $this; } @@ -322,7 +322,6 @@ final class CoreExpectation */ public function toBeInstanceOf(string $class): CoreExpectation { - /* @phpstan-ignore-next-line */ Assert::assertInstanceOf($class, $this->value); return $this; diff --git a/src/Exceptions/ExpectationNotFoundException.php b/src/Exceptions/ExpectationNotFoundException.php index 5b9c61ec..07bafdc3 100644 --- a/src/Exceptions/ExpectationNotFoundException.php +++ b/src/Exceptions/ExpectationNotFoundException.php @@ -1,5 +1,7 @@ value) ? $this->value : iterator_to_array($this->value); - $keys = array_keys($value); - $values = array_values($value); + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; - $index = $index < count($values) - 1 ? $index + 1 : 0; + $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { @@ -214,7 +212,7 @@ final class Expectation $condition = is_callable($condition) ? $condition : static function () use ($condition): bool { - return $condition; // @phpstan-ignore-line + return $condition; }; return $this->when(!$condition(), $callback); @@ -231,7 +229,7 @@ final class Expectation $condition = is_callable($condition) ? $condition : static function () use ($condition): bool { - return $condition; // @phpstan-ignore-line + return $condition; }; if ($condition()) { @@ -256,7 +254,10 @@ final class Expectation return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } - $this->getExpectationClosure($method)(...$parameters); + ExpectationPipeline::for($this->getExpectationClosure($method)) + ->send(...$parameters) + ->through($this->pipes($method, $this, Expectation::class)) + ->run(); return $this; } @@ -295,7 +296,7 @@ final class Expectation private function getExpectationClosure(string $name): Closure { if (method_exists($this->coreExpectation, $name)) { - /** @phpstan-ignore-next-line */ + /* @phpstan-ignore-next-line */ return Closure::fromCallable([$this->coreExpectation, $name]); } diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php new file mode 100644 index 00000000..557c82e5 --- /dev/null +++ b/src/Support/ExpectationPipeline.php @@ -0,0 +1,65 @@ + */ + private array $pipes = []; + + /** @var array */ + private array $passable; + + public function __construct( + private Closure $expectationClosure + ) { + //.. + } + + public static function for(Closure $expectationClosure): ExpectationPipeline + { + return new self($expectationClosure); + } + + public function send(...$passable): ExpectationPipeline + { + $this->passable = $passable; + + return $this; + } + + /** + * @param array $pipes + */ + public function through(array $pipes): ExpectationPipeline + { + $this->pipes = $pipes; + + return $this; + } + + public function run(): void + { + $pipeline = array_reduce( + array_reverse($this->pipes), + $this->carry(), + function (): void { + ($this->expectationClosure)(...$this->passable); + } + ); + + $pipeline(); + } + + public function carry(): Closure + { + return fn ($stack, $pipe): Closure => fn () => $pipe($stack, ...$this->passable); + } +} diff --git a/src/Support/Extendable.php b/src/Support/Extendable.php index 094d7007..17150050 100644 --- a/src/Support/Extendable.php +++ b/src/Support/Extendable.php @@ -24,4 +24,17 @@ 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 + { + $this->extendableClass::pipe($name, $handler); + } + + public function intercept(string $name, string|Closure $filter, Closure $handler): void + { + $this->extendableClass::intercept($name, $filter, $handler); + } } diff --git a/tests/Features/Expect/pipes.php b/tests/Features/Expect/pipes.php new file mode 100644 index 00000000..c98d25fe --- /dev/null +++ b/tests/Features/Expect/pipes.php @@ -0,0 +1,255 @@ +reset(); + } + + public function reset(): void + { + $this->appliedCount = $this->runCount = [ + 'char' => 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['char']++; + + if ($this->value instanceof Char) { + $state->appliedCount['char']++; + + assertInstanceOf(Char::class, $expected); + assertEquals($this->value->value, $expected->value); + + //returning nothing stops pipeline execution + return; + } + + //calling $next(); let the pipeline to keep running + $next(); +}); + +/* + * Overrides toBe to assert two Number objects are the same + */ +expect()->intercept('toBe', Number::class, function ($expected) use ($state) { + $state->runCount['number']++; + $state->appliedCount['number']++; + + assertInstanceOf(Number::class, $expected); + assertEquals($this->value->value, $expected->value); +}); + +/* + * Overrides toBe to assert all integers are allowed if value is a wildcard (*) + */ +expect()->intercept('toBe', fn ($value, $expected) => $value === '*' && is_numeric($expected), function ($expected) use ($state) { + $state->runCount['wildcard']++; + $state->appliedCount['wildcard']++; +}); + +/* + * Overrides toBe to assert to 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 to allow ignoring case when checking strings + */ +expect()->intercept('toBe', fn ($value) => 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) { + $char = new Char('A'); + + $state->reset(); + + expect($char)->toBe(new Char('A')) + ->and($state) + ->runCount->toMatchArray([ + 'char' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, + ]) + ->appliedCount->toMatchArray([ + 'char' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, + ]); +}); + +test('pipe is run and lets the pipeline to keep going', function () use ($state) { + $state->reset(); + + expect(3)->toBe(3) + ->and($state) + ->runCount->toMatchArray([ + 'char' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 1, + ]) + ->appliedCount->toMatchArray([ + 'char' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, + ]); +}); + +test('pipe works with negated expectation', function () use ($state) { + $char = new Char('A'); + + $state->reset(); + + expect($char)->not->toBe(new Char('B')) + ->and($state) + ->runCount->toMatchArray([ + 'char' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, + ]) + ->appliedCount->toMatchArray([ + 'char' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, + ]); +}); + +test('interceptor 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('interceptor stops the pipeline', function () use ($state) { + $number = new Number(1); + + $state->reset(); + + expect($number)->toBe(new Number(1)) + ->and($state) + ->runCount->toMatchArray([ + 'char' => 1, + 'number' => 1, + 'wildcard' => 0, + 'symbol' => 0, + ]) + ->appliedCount->toMatchArray([ + 'char' => 0, + 'number' => 1, + 'wildcard' => 0, + 'symbol' => 0, + ]); +}); + +test('interceptor 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('interceptor 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('interceptor can be filter the expected parameter as well', function () use ($state) { + $state->reset(); + + expect('*')->toBe('*') + ->and($state) + ->runCount->toHaveKey('wildcard', 0) + ->appliedCount->toHaveKey('wildcard', 0); +}); + +test('interceptor works with negated expectation', function () { + $char = new Number(1); + + expect($char)->not->toBe(new Char('B')); +}); + +test('intercept can add new parameters to the expectation', function () { + $ignoreCase = true; + + expect('Foo')->toBe('foo', $ignoreCase); +}); From 4a22c5d6731ef3ec24d873a8c11269774b9e18af Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 31 Oct 2021 22:08:34 +0100 Subject: [PATCH 14/45] addresses reviews --- src/Concerns/Extendable.php | 22 +++++++++++----------- src/Support/ExpectationPipeline.php | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 792be193..ead03d01 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -7,7 +7,6 @@ namespace Pest\Concerns; use BadMethodCallException; use Closure; use Pest\Expectation; -use PHPStan\Type\CallableType; /** * @internal @@ -50,6 +49,9 @@ trait Extendable /** * 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 { @@ -58,19 +60,17 @@ trait Extendable } self::pipe($name, function ($next, ...$arguments) use ($handler, $filter): void { - /** @phpstan-ignore-next-line */ - if ($filter($this->value, ...$arguments)) { - /** @phpstan-ignore-next-line */ - $handler = $handler->bindTo($this, get_class($this)); - - if($handler instanceof Closure){ - $handler(...$arguments); - } + /* @phpstan-ignore-next-line */ + if (!$filter($this->value, ...$arguments)) { + $next(); return; } - $next(); + /** @phpstan-ignore-next-line */ + $handler = $handler->bindTo($this, $this::class); + + $handler(...$arguments); }); } @@ -97,7 +97,7 @@ trait Extendable foreach (self::$pipes[$name] as $pipe) { $pipe = $pipe->bindTo($context, $scope); - if($pipe instanceof Closure){ + if ($pipe instanceof Closure) { $pipes[] = $pipe; } } diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php index 557c82e5..144a2c0f 100644 --- a/src/Support/ExpectationPipeline.php +++ b/src/Support/ExpectationPipeline.php @@ -14,7 +14,7 @@ final class ExpectationPipeline /** @var array */ private array $pipes = []; - /** @var array */ + /** @var array */ private array $passable; public function __construct( @@ -28,7 +28,7 @@ final class ExpectationPipeline return new self($expectationClosure); } - public function send(...$passable): ExpectationPipeline + public function send(mixed ...$passable): ExpectationPipeline { $this->passable = $passable; From 5f1776829b7be83f81503f8ace67a07bc663cf2d Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 31 Oct 2021 22:16:02 +0100 Subject: [PATCH 15/45] Update src/Concerns/Extendable.php --- src/Concerns/Extendable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index ead03d01..5a8408cd 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -77,7 +77,7 @@ trait Extendable /** * Checks if pipes are registered for a given expectation. */ - public static function hasPipes(string $name): bool + private static function hasPipes(string $name): bool { return array_key_exists($name, static::$pipes); } From 602403eb59d1d0e38fab674370baa0a9a864d87c Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 1 Nov 2021 10:31:30 +0100 Subject: [PATCH 16/45] Update src/Concerns/Extendable.php Co-authored-by: Luke Downing --- src/Concerns/Extendable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 5a8408cd..a8ff0ab0 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -20,7 +20,7 @@ trait Extendable */ private static array $extends = []; - /** @var array> */ + /** @var array> */ private static array $pipes = []; /** From be58d5517a738150e1340bb487f2c4e75a4b431d Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 1 Nov 2021 10:40:39 +0100 Subject: [PATCH 17/45] improves static analysis --- src/Concerns/Extendable.php | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index a8ff0ab0..031c8b30 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -20,7 +20,7 @@ trait Extendable */ private static array $extends = []; - /** @var array> */ + /** @var array> */ private static array $pipes = []; /** @@ -60,28 +60,20 @@ trait Extendable } self::pipe($name, function ($next, ...$arguments) use ($handler, $filter): void { - /* @phpstan-ignore-next-line */ + /* @phpstan-ignore-next-line */ if (!$filter($this->value, ...$arguments)) { $next(); return; } - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore-next-line */ $handler = $handler->bindTo($this, $this::class); $handler(...$arguments); }); } - /** - * Checks if pipes are registered for a given expectation. - */ - private static function hasPipes(string $name): bool - { - return array_key_exists($name, static::$pipes); - } - /** * Gets the pipes that have been registered for a given expectation and binds them to a context and a scope. * @@ -89,20 +81,7 @@ trait Extendable */ private function pipes(string $name, object $context, string $scope): array { - if (!self::hasPipes($name)) { - return []; - } - - $pipes = []; - foreach (self::$pipes[$name] as $pipe) { - $pipe = $pipe->bindTo($context, $scope); - - if ($pipe instanceof Closure) { - $pipes[] = $pipe; - } - } - - return $pipes; + return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } /** From 22895ce6826799b0aa98ee301a1f0ec998b6b51d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 17 Nov 2021 10:55:45 +0000 Subject: [PATCH 18/45] docs: updates release --- RELEASE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 1d651d7c..81d3c198 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,7 +2,9 @@ When releasing a new version of Pest there are some checks and updates that need to be done: -- Clear your local repository with: `git add . && git reset --hard && git checkout master` +> **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) - Run the tests locally using: `composer test` From b9b9de1945bde28db597f24bd44f25d76c3be463 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 15 Nov 2021 12:23:53 +0100 Subject: [PATCH 19/45] upgrade to phpstan level 6 --- phpstan.neon | 2 +- src/Datasets.php | 17 +++++++++-------- src/Factories/Annotations/Depends.php | 4 ++++ src/Factories/Annotations/Groups.php | 4 ++++ src/Factories/TestCaseFactory.php | 2 ++ src/Repositories/TestRepository.php | 2 +- src/Support/Arr.php | 4 ++++ 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index c1a0e56e..4594d0d2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: 5 + level: 6 paths: - src diff --git a/src/Datasets.php b/src/Datasets.php index 429de157..09e0219f 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -9,6 +9,7 @@ use Pest\Exceptions\DatasetAlreadyExist; use Pest\Exceptions\DatasetDoesNotExist; use SebastianBergmann\Exporter\Exporter; use Traversable; +use function sprintf; /** * @internal @@ -25,14 +26,14 @@ final class Datasets /** * Holds the withs. * - * @var array + * @var array|string> */ private static array $withs = []; /** * Sets the given. * - * @param Closure|iterable $data + * @phpstan-param Closure|iterable $data */ public static function set(string $name, Closure|iterable $data): void { @@ -46,7 +47,7 @@ final class Datasets /** * Sets the given. * - * @param Closure|iterable|string $with + * @phpstan-param Closure|iterable|string $with */ public static function with(string $filename, string $description, Closure|iterable|string $with): void { @@ -119,7 +120,7 @@ final class Datasets /** * @param array|string> $datasets * - * @return array + * @return array> */ private static function processDatasets(array $datasets): array { @@ -159,9 +160,9 @@ final class Datasets } /** - * @param array $combinations + * @param array> $combinations * - * @return array + * @return array> */ private static function getDataSetsCombinations(array $combinations): array { @@ -187,9 +188,9 @@ final class Datasets $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); } } diff --git a/src/Factories/Annotations/Depends.php b/src/Factories/Annotations/Depends.php index 15b29359..66c4838c 100644 --- a/src/Factories/Annotations/Depends.php +++ b/src/Factories/Annotations/Depends.php @@ -14,6 +14,10 @@ final class Depends { /** * Adds annotations regarding the "depends" feature. + * + * @param array $annotations + * + * @return array */ public function add(TestCaseMethodFactory $method, array $annotations): array { diff --git a/src/Factories/Annotations/Groups.php b/src/Factories/Annotations/Groups.php index 96752d6e..ab7749b7 100644 --- a/src/Factories/Annotations/Groups.php +++ b/src/Factories/Annotations/Groups.php @@ -13,6 +13,10 @@ final class Groups { /** * Adds annotations regarding the "groups" feature. + * + * @param array $annotations + * + * @return array */ public function add(TestCaseMethodFactory $method, array $annotations): array { diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index d21b5513..edb37fd3 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -98,6 +98,8 @@ final class TestCaseFactory /** * Creates a Test Case class using a runtime evaluate. + * + * @param array $methods */ public function evaluate(string $filename, array $methods): string { diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index b8e2b53e..3a65c529 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -80,7 +80,7 @@ final class TestRepository } } - public function get($filename): TestCaseFactory + public function get(string $filename): TestCaseFactory { return $this->testCases[$filename]; } diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 3fbe0e61..531e0d19 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -11,6 +11,8 @@ final class Arr { /** * Checks if the given array has the given key. + * + * @param array $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 */ public static function get(array $array, string|int $key, mixed $default = null): mixed { From b205b8e74896bbc5b4126076212b8142157934a8 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 15 Nov 2021 20:54:47 +0100 Subject: [PATCH 20/45] trying to disable phpstan parallel processing --- phpstan.neon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpstan.neon b/phpstan.neon index 4594d0d2..eda92dbf 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,6 +7,8 @@ parameters: level: 6 paths: - src + parallel: + maximumNumberOfProcesses: 1 checkMissingIterableValueType: true checkGenericClassInNonGenericObjectType: false From ca30677c53be2a10674f8f262013e2ccd021feef Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 15 Nov 2021 22:20:00 +0100 Subject: [PATCH 21/45] upgrade to phpstan lvl 7 --- phpstan.neon | 2 +- src/Bootstrappers/BootSubscribers.php | 2 +- src/Datasets.php | 10 +++++----- src/Factories/TestCaseFactory.php | 1 + src/Functions.php | 2 ++ src/Kernel.php | 1 + src/PendingCalls/UsesCall.php | 4 ++-- src/Plugin.php | 2 ++ src/Repositories/TestRepository.php | 2 +- src/Support/Container.php | 5 +++++ src/Support/HigherOrderMessage.php | 2 +- 11 files changed, 22 insertions(+), 11 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index eda92dbf..1b341db4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: 6 + level: 7 paths: - src parallel: diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index c23e9bf0..42bdeaba 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -29,7 +29,7 @@ final class BootSubscribers { foreach (self::$subscribers as $subscriber) { Event\Facade::registerSubscriber( - new $subscriber() + new $subscriber() //@phpstan-ignore-line ); } } diff --git a/src/Datasets.php b/src/Datasets.php index 09e0219f..96aa00dd 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -8,8 +8,8 @@ use Closure; use Pest\Exceptions\DatasetAlreadyExist; use Pest\Exceptions\DatasetDoesNotExist; use SebastianBergmann\Exporter\Exporter; -use Traversable; use function sprintf; +use Traversable; /** * @internal @@ -19,14 +19,14 @@ final class Datasets /** * Holds the datasets. * - * @var array> + * @var array> */ private static array $datasets = []; /** * Holds the withs. * - * @var array|string> + * @var array|string>> */ private static array $withs = []; @@ -47,9 +47,9 @@ final class Datasets /** * Sets the given. * - * @phpstan-param Closure|iterable|string $with + * @phpstan-param array|string> $with */ - public static function with(string $filename, string $description, Closure|iterable|string $with): void + public static function with(string $filename, string $description, array $with): void { self::$withs[$filename . '>>>' . $description] = $with; } diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index edb37fd3..5b077d08 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -148,6 +148,7 @@ final class TestCaseFactory $annotations = ['@test']; foreach (self::$annotations as $annotation) { + //@phpstan-ignore-next-line $annotations = (new $annotation())->add($method, $annotations); } diff --git a/src/Functions.php b/src/Functions.php index 1e306999..ddc6f3af 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -70,6 +70,8 @@ if (!function_exists('uses')) { /** * The uses function binds the given * arguments to test closures. + * + * @param class-string ...$classAndTraits */ function uses(string ...$classAndTraits): UsesCall { diff --git a/src/Kernel.php b/src/Kernel.php index af2d82e6..67c4dcf9 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -37,6 +37,7 @@ final class Kernel public static function boot(): self { foreach (self::$bootstrappers as $bootstrapper) { + //@phpstan-ignore-next-line (new $bootstrapper())->__invoke(); } diff --git a/src/PendingCalls/UsesCall.php b/src/PendingCalls/UsesCall.php index 6609e8b2..457f8699 100644 --- a/src/PendingCalls/UsesCall.php +++ b/src/PendingCalls/UsesCall.php @@ -36,14 +36,14 @@ final class UsesCall /** * Holds the groups of the uses. * - * @var array + * @var array */ private array $groups = []; /** * Creates a new Pending Call. * - * @param array $classAndTraits + * @param array $classAndTraits */ public function __construct( private string $filename, diff --git a/src/Plugin.php b/src/Plugin.php index 9de1f707..5f676be4 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -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 { diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 3a65c529..869d1000 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -23,7 +23,7 @@ final class TestRepository private array $testCases = []; /** - * @var array>> + * @var array, 1: array, 2: array}> */ private array $uses = []; diff --git a/src/Support/Container.php b/src/Support/Container.php index 6390484b..b87fdd65 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -35,6 +35,8 @@ final class Container /** * Gets a dependency from the container. * + * @param class-string $id + * * @return object */ public function get(string $id) @@ -60,6 +62,8 @@ final class Container /** * Tries to build the given instance. + * + * @param class-string $id */ private function build(string $id): object { @@ -83,6 +87,7 @@ final class Container } } + //@phpstan-ignore-next-line return $this->get($candidate); }, $constructor->getParameters() diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index f756c64d..575eaf98 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -77,7 +77,7 @@ final class HigherOrderMessage */ public function when(callable $condition): self { - $this->condition = $condition; + $this->condition = Closure::fromCallable($condition); return $this; } From 83b9f869727f6a1afe25b6ac93cadb539d97b5b9 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 18 Nov 2021 00:12:39 +0100 Subject: [PATCH 22/45] upgrade to phpstan lvl 8 --- phpstan.neon | 2 +- src/Datasets.php | 9 +++++++- src/Factories/TestCaseFactory.php | 12 +++++++++++ src/Factories/TestCaseMethodFactory.php | 2 +- src/Support/ChainableClosure.php | 8 +++---- src/Support/Closure.php | 28 +++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 src/Support/Closure.php diff --git a/phpstan.neon b/phpstan.neon index 1b341db4..89377d38 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: 7 + level: 8 paths: - src parallel: diff --git a/src/Datasets.php b/src/Datasets.php index 96aa00dd..c91de5e7 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -7,6 +7,7 @@ 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; @@ -61,7 +62,13 @@ final class Datasets { $dataset = self::$withs[$filename . '>>>' . $description]; - return self::resolve($description, $dataset); + $dataset = self::resolve($description, $dataset); + + if ($dataset === null) { + throw ShouldNotHappen::fromMessage('Could not resolve dataset.'); + } + + return $dataset; } /** diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 5b077d08..517a586c 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -142,6 +142,10 @@ final class TestCaseFactory } $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 = ''; @@ -224,6 +228,10 @@ EOF; } 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) { @@ -240,6 +248,10 @@ EOF; 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; } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index ffa4cb4b..e2ece273 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -89,7 +89,7 @@ final class TestCaseMethodFactory $testCase->chains->chain($this); $method->chains->chain($this); - return call_user_func(Closure::bind($closure, $this, $this::class), ...func_get_args()); + return \Pest\Support\Closure::safeBind($closure, $this, $this::class)(...func_get_args()); }; } diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index 8a136682..b3406d86 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -22,8 +22,8 @@ final class ChainableClosure throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); } - call_user_func_array(Closure::bind($closure, $this, $this::class), func_get_args()); - call_user_func_array(Closure::bind($next, $this, $this::class), func_get_args()); + \Pest\Support\Closure::safeBind($closure, $this, $this::class)(...func_get_args()); + \Pest\Support\Closure::safeBind($next, $this, $this::class)(...func_get_args()); }; } @@ -33,8 +33,8 @@ final class ChainableClosure public static function fromStatic(Closure $closure, Closure $next): Closure { return static function () use ($closure, $next): void { - call_user_func_array(Closure::bind($closure, null, self::class), func_get_args()); - call_user_func_array(Closure::bind($next, null, self::class), func_get_args()); + \Pest\Support\Closure::safeBind($closure, null, self::class)(...func_get_args()); + \Pest\Support\Closure::safeBind($next, null, self::class)(...func_get_args()); }; } } diff --git a/src/Support/Closure.php b/src/Support/Closure.php new file mode 100644 index 00000000..ece77d57 --- /dev/null +++ b/src/Support/Closure.php @@ -0,0 +1,28 @@ + Date: Thu, 18 Nov 2021 01:01:56 +0100 Subject: [PATCH 23/45] wip toward lvl9 --- src/Exceptions/ExpectationException.php | 11 + src/Expectation.php | 753 ++++++++++++++++++++++-- src/Functions.php | 5 +- src/HigherOrderExpectation.php | 5 +- src/Plugins/Coverage.php | 5 +- src/Support/Container.php | 8 +- src/Support/ExceptionTrace.php | 2 + src/Support/HigherOrderCallables.php | 5 +- 8 files changed, 726 insertions(+), 68 deletions(-) create mode 100644 src/Exceptions/ExpectationException.php diff --git a/src/Exceptions/ExpectationException.php b/src/Exceptions/ExpectationException.php new file mode 100644 index 00000000..dfe63bb9 --- /dev/null +++ b/src/Exceptions/ExpectationException.php @@ -0,0 +1,11 @@ +coreExpectation = new CoreExpectation($value); + public function __construct( + public mixed $value + ) { + // .. } /** @@ -60,13 +69,17 @@ final class Expectation */ public function json(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidValue('json', 'string'); + } + return $this->toBeJson()->and(json_decode($this->value, true)); } /** * Dump the expectation value and end the script. * - * @phpstan-return never + * @return never */ public function dd(mixed ...$arguments): void { @@ -122,7 +135,7 @@ final class Expectation * * @template TSequenceValue * - * @phpstan-param (callable(self, self): void)|TSequenceValue ...$callbacks + * @param (callable(self, self): void)|TSequenceValue ...$callbacks */ public function sequence(mixed ...$callbacks): Expectation { @@ -172,7 +185,7 @@ final class Expectation ? $subject : fn () => $subject; - $subject = $subject(); + $subject = $subject(); $matched = false; @@ -203,8 +216,8 @@ final class Expectation /** * Apply the callback if the given "condition" is falsy. * - * @phpstan-param (callable(): bool)|bool $condition - * @phpstan-param callable(Expectation): mixed $callback + * @param (callable(): bool)|bool $condition + * @param callable(Expectation): mixed $callback */ public function unless(callable|bool $condition, callable $callback): Expectation { @@ -220,8 +233,8 @@ final class Expectation /** * Apply the callback if the given "condition" is truthy. * - * @phpstan-param (callable(): bool)|bool $condition - * @phpstan-param callable(Expectation): mixed $callback + * @param (callable(): bool)|bool $condition + * @param callable(Expectation): mixed $callback */ public function when(callable|bool $condition, callable $callback): Expectation { @@ -239,58 +252,685 @@ final class Expectation } /** - * Dynamically handle calls to the class or - * creates a new higher order expectation. - * - * @phpstan-param array $parameters - * - * @return HigherOrderExpectation|Expectation + * Asserts that two variables have the same type and + * value. Used on objects, it asserts that two + * variables reference the same object. */ - public function __call(string $method, array $parameters) + public function toBe(mixed $expected): Expectation { - if (!$this->hasExpectation($method)) { - /* @phpstan-ignore-next-line */ - return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); - } - - ExpectationPipeline::for($method, $this->getExpectationClosure($method)) - ->send(...$parameters) - ->through($this->pipes($method, $this, Expectation::class)) - ->run(); + Assert::assertSame($expected, $this->value); return $this; } - private function getExpectationClosure(string $name): Closure + /** + * Asserts that the value is empty. + */ + public function toBeEmpty(): Expectation { - if (method_exists($this->coreExpectation, $name)) { - //@phpstan-ignore-next-line - return Closure::fromCallable([$this->coreExpectation, $name]); - } + Assert::assertEmpty($this->value); - if (self::hasExtend($name)) { - $extend = self::$extends[$name]->bindTo($this, Expectation::class); + return $this; + } - if ($extend != false) { - return $extend; + /** + * Asserts that the value is true. + */ + public function toBeTrue(): Expectation + { + Assert::assertTrue($this->value); + + return $this; + } + + /** + * Asserts that the value is truthy. + */ + public function toBeTruthy(): Expectation + { + Assert::assertTrue((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is false. + */ + public function toBeFalse(): Expectation + { + Assert::assertFalse($this->value); + + return $this; + } + + /** + * Asserts that the value is falsy. + */ + public function toBeFalsy(): Expectation + { + Assert::assertFalse((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than $expected. + */ + public function toBeGreaterThan(int|float $expected): Expectation + { + Assert::assertGreaterThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than or equal to $expected. + */ + public function toBeGreaterThanOrEqual(int|float $expected): Expectation + { + Assert::assertGreaterThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than or equal to $expected. + */ + public function toBeLessThan(int|float $expected): Expectation + { + Assert::assertLessThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than $expected. + */ + public function toBeLessThanOrEqual(int|float $expected): Expectation + { + Assert::assertLessThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that $needle is an element of the value. + */ + public function toContain(mixed ...$needles): Expectation + { + foreach ($needles as $needle) { + if (is_string($this->value)) { + Assert::assertStringContainsString($needle, $this->value); + } else { + Assert::assertContains($needle, $this->value); } } - throw PipeException::expectationNotFound($name); + return $this; } - - private function hasExpectation(string $name): bool + /** + * Asserts that the value starts with $expected. + * + * @param non-empty-string $expected + */ + public function toStartWith(string $expected): Expectation { - if (method_exists($this->coreExpectation, $name)) { - return true; + Assert::assertStringStartsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value ends with $expected. + * + * @param non-empty-string $expected + */ + public function toEndWith(string $expected): Expectation + { + Assert::assertStringEndsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that $number matches value's Length. + */ + public function toHaveLength(int $number): Expectation + { + if (is_string($this->value)) { + Assert::assertEquals($number, mb_strlen($this->value)); + + return $this; } - if (self::hasExtend($name)) { - return true; + if (is_iterable($this->value)) { + return $this->toHaveCount($number); } - return false; + if (is_object($this->value)) { + if (method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + Assert::assertCount($number, $array); + + return $this; + } + + throw new BadMethodCallException('Expectation value length is not countable.'); + } + + /** + * Asserts that $count matches the number of elements of the value. + */ + public function toHaveCount(int $count): Expectation + { + Assert::assertCount($count, $this->value); + + return $this; + } + + /** + * Asserts that the value contains the property $name. + */ + public function toHaveProperty(string $name, mixed $value = null): Expectation + { + $this->toBeObject(); + + Assert::assertTrue(property_exists($this->value, $name)); + + if (func_num_args() > 1) { + /* @phpstan-ignore-next-line */ + Assert::assertEquals($value, $this->value->{$name}); + } + + return $this; + } + + /** + * Asserts that the value contains the provided properties $names. + * + * @param iterable $names + */ + public function toHaveProperties(iterable $names): Expectation + { + foreach ($names as $name) { + $this->toHaveProperty($name); + } + + return $this; + } + + /** + * Asserts that two variables have the same value. + */ + public function toEqual(mixed $expected): Expectation + { + Assert::assertEquals($expected, $this->value); + + return $this; + } + + /** + * Asserts that two variables have the same value. + * The contents of $expected and the $this->value are + * canonicalized before they are compared. For instance, when the two + * variables $expected and $this->value are arrays, then these arrays + * are sorted before they are compared. When $expected and $this->value + * are objects, each object is converted to an array containing all + * private, protected and public attributes. + */ + public function toEqualCanonicalizing(mixed $expected): Expectation + { + Assert::assertEqualsCanonicalizing($expected, $this->value); + + return $this; + } + + /** + * Asserts that the absolute difference between the value and $expected + * is lower than $delta. + */ + public function toEqualWithDelta(mixed $expected, float $delta): Expectation + { + Assert::assertEqualsWithDelta($expected, $this->value, $delta); + + return $this; + } + + /** + * Asserts that the value is one of the given values. + * + * @param iterable $values + */ + public function toBeIn(iterable $values): Expectation + { + Assert::assertContains($this->value, $values); + + return $this; + } + + /** + * Asserts that the value is infinite. + */ + public function toBeInfinite(): Expectation + { + Assert::assertInfinite($this->value); + + return $this; + } + + /** + * Asserts that the value is an instance of $class. + * + * @param class-string $class + */ + public function toBeInstanceOf(string $class): Expectation + { + Assert::assertInstanceOf($class, $this->value); + + return $this; + } + + /** + * Asserts that the value is an array. + */ + public function toBeArray(): Expectation + { + Assert::assertIsArray($this->value); + + return $this; + } + + /** + * Asserts that the value is of type bool. + */ + public function toBeBool(): Expectation + { + Assert::assertIsBool($this->value); + + return $this; + } + + /** + * Asserts that the value is of type callable. + */ + public function toBeCallable(): Expectation + { + Assert::assertIsCallable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type float. + */ + public function toBeFloat(): Expectation + { + Assert::assertIsFloat($this->value); + + return $this; + } + + /** + * Asserts that the value is of type int. + */ + public function toBeInt(): Expectation + { + Assert::assertIsInt($this->value); + + return $this; + } + + /** + * Asserts that the value is of type iterable. + */ + public function toBeIterable(): Expectation + { + Assert::assertIsIterable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type numeric. + */ + public function toBeNumeric(): Expectation + { + Assert::assertIsNumeric($this->value); + + return $this; + } + + /** + * Asserts that the value is of type object. + */ + public function toBeObject(): Expectation + { + Assert::assertIsObject($this->value); + + return $this; + } + + /** + * Asserts that the value is of type resource. + */ + public function toBeResource(): Expectation + { + Assert::assertIsResource($this->value); + + return $this; + } + + /** + * Asserts that the value is of type scalar. + */ + public function toBeScalar(): Expectation + { + Assert::assertIsScalar($this->value); + + return $this; + } + + /** + * Asserts that the value is of type string. + */ + public function toBeString(): Expectation + { + Assert::assertIsString($this->value); + + return $this; + } + + /** + * Asserts that the value is a JSON string. + */ + public function toBeJson(): Expectation + { + Assert::assertIsString($this->value); + Assert::assertJson($this->value); + + return $this; + } + + /** + * Asserts that the value is NAN. + */ + public function toBeNan(): Expectation + { + Assert::assertNan($this->value); + + return $this; + } + + /** + * Asserts that the value is null. + */ + public function toBeNull(): Expectation + { + Assert::assertNull($this->value); + + return $this; + } + + /** + * Asserts that the value array has the provided $key. + */ + public function toHaveKey(string|int $key, mixed $value = null): Expectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + try { + Assert::assertTrue(Arr::has($array, $key)); + + /* @phpstan-ignore-next-line */ + } catch (ExpectationFailedException $exception) { + throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); + } + + if (func_num_args() > 1) { + Assert::assertEquals($value, Arr::get($array, $key)); + } + + return $this; + } + + /** + * Asserts that the value array has the provided $keys. + * + * @param array $keys + */ + public function toHaveKeys(array $keys): Expectation + { + foreach ($keys as $key) { + $this->toHaveKey($key); + } + + return $this; + } + + /** + * Asserts that the value is a directory. + */ + public function toBeDirectory(): Expectation + { + Assert::assertDirectoryExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is readable. + */ + public function toBeReadableDirectory(): Expectation + { + Assert::assertDirectoryIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is writable. + */ + public function toBeWritableDirectory(): Expectation + { + Assert::assertDirectoryIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file. + */ + public function toBeFile(): Expectation + { + Assert::assertFileExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is readable. + */ + public function toBeReadableFile(): Expectation + { + Assert::assertFileIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is writable. + */ + public function toBeWritableFile(): Expectation + { + Assert::assertFileIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value array matches the given array subset. + * + * @param iterable $array + */ + public function toMatchArray(iterable|object $array): Expectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $valueAsArray = $this->value->toArray(); + } else { + $valueAsArray = (array) $this->value; + } + + foreach ($array as $key => $value) { + Assert::assertArrayHasKey($key, $valueAsArray); + + Assert::assertEquals( + $value, + $valueAsArray[$key], + sprintf( + 'Failed asserting that an array has a key %s with the value %s.', + $this->export($key), + $this->export($valueAsArray[$key]), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value object matches a subset + * of the properties of an given object. + * + * @param iterable|object $object + */ + public function toMatchObject(iterable|object $object): Expectation + { + foreach ((array) $object as $property => $value) { + Assert::assertTrue(property_exists($this->value, $property)); + + /* @phpstan-ignore-next-line */ + $propertyValue = $this->value->{$property}; + Assert::assertEquals( + $value, + $propertyValue, + sprintf( + 'Failed asserting that an object has a property %s with the value %s.', + $this->export($property), + $this->export($propertyValue), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value matches a regular expression. + */ + public function toMatch(string $expression): Expectation + { + Assert::assertMatchesRegularExpression($expression, $this->value); + + return $this; + } + + /** + * Asserts that the value matches a constraint. + */ + public function toMatchConstraint(Constraint $constraint): Expectation + { + Assert::assertThat($this->value, $constraint); + + return $this; + } + + /** + * Asserts that executing value throws an exception. + * + * @param (Closure(Throwable): mixed)|string $exception + */ + public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation + { + $callback = NullClosure::create(); + + if ($exception instanceof Closure) { + $callback = $exception; + $parameters = (new ReflectionFunction($exception))->getParameters(); + + if (1 !== count($parameters)) { + throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); + } + + if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { + throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); + } + + $exception = $type->getName(); + } + + try { + ($this->value)(); + } catch (Throwable $e) { // @phpstan-ignore-line + if (!class_exists($exception)) { + Assert::assertStringContainsString($exception, $e->getMessage()); + + return $this; + } + + if ($exceptionMessage !== null) { + Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); + } + + Assert::assertInstanceOf($exception, $e); + $callback($e); + + return $this; + } + + if (!class_exists($exception)) { + throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); + } + + throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); + } + + /** + * Exports the given value. + */ + private function export(mixed $value): string + { + if ($this->exporter === null) { + $this->exporter = new Exporter(); + } + + return $this->exporter->export($value); + } + + /** + * Dynamically handle calls to the class or + * creates a new higher order expectation. + * + * @param array $parameters + * + * @return HigherOrderExpectation|mixed + */ + public function __call(string $method, array $parameters) + { + if (!static::hasExtend($method)) { + /* @phpstan-ignore-next-line */ + return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); + } + + return $this->__extendsCall($method, $parameters); } /** @@ -299,20 +939,11 @@ final class Expectation */ public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation { - if ($name === 'value') { - return $this->coreExpectation->value; - } - - if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !self::hasExtend($name)) { + if (!method_exists($this, $name) && !static::hasExtend($name)) { return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } /* @phpstan-ignore-next-line */ return $this->{$name}(); } - - public static function hasMethod(string $name): bool - { - return method_exists(CoreExpectation::class, $name); - } } diff --git a/src/Functions.php b/src/Functions.php index ddc6f3af..0f72cee7 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -113,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; } } diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index dc8b2d6d..7abd9d1d 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -80,7 +80,10 @@ final class HigherOrderExpectation } if (!$this->expectationHasMethod($name)) { - return new self($this->original, $this->retrieve($name, $this->getValue())); + /** @var array|object $value */ + $value = $this->getValue(); + + return new self($this->original, $this->retrieve($name, $value)); } return $this->performAssertion($name, []); diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index 8febde52..f2ee5457 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -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 $min_option */ + $min_option = $input->getOption(self::MIN_OPTION); + + $this->coverageMin = (float) $min_option; } return $originals; diff --git a/src/Support/Container.php b/src/Support/Container.php index b87fdd65..6f2afc13 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -41,11 +41,13 @@ final class Container */ 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); } - $this->instances[$id] = $this->build($id); + if (!is_object($this->instances[$id])) { + throw ShouldNotHappen::fromMessage('Cannot resolve a non-object from container'); + } return $this->instances[$id]; } diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index ec17afc8..17fc2e40 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -50,6 +50,8 @@ final class ExceptionTrace $property = new ReflectionProperty($t, 'serializableTrace'); $property->setAccessible(true); + + /** @var array> $trace */ $trace = $property->getValue($t); $cleanedTrace = []; diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php index 1637ccb2..be02b86d 100644 --- a/src/Support/HigherOrderCallables.php +++ b/src/Support/HigherOrderCallables.php @@ -31,7 +31,10 @@ final class HigherOrderCallables */ 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); } /** From 24edab45b14b87af73d4d3cb17109babb453c8eb Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 18 Nov 2021 01:04:59 +0100 Subject: [PATCH 24/45] fix tests --- src/Exceptions/ExpectationException.php | 9 +++++++-- src/Support/Container.php | 4 ---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Exceptions/ExpectationException.php b/src/Exceptions/ExpectationException.php index dfe63bb9..ca14ce76 100644 --- a/src/Exceptions/ExpectationException.php +++ b/src/Exceptions/ExpectationException.php @@ -1,10 +1,15 @@ instances[$id] = $this->build($id); } - if (!is_object($this->instances[$id])) { - throw ShouldNotHappen::fromMessage('Cannot resolve a non-object from container'); - } - return $this->instances[$id]; } From 9258dcc98830b077e26e37406cc4b23abc523992 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 18 Nov 2021 01:14:57 +0100 Subject: [PATCH 25/45] fix phpstan failure --- phpstan.neon | 2 -- src/OppositeExpectation.php | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 89377d38..362ba447 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,8 +7,6 @@ parameters: level: 8 paths: - src - parallel: - maximumNumberOfProcesses: 1 checkMissingIterableValueType: true checkGenericClassInNonGenericObjectType: false diff --git a/src/OppositeExpectation.php b/src/OppositeExpectation.php index e4ec3d85..da9ec6ab 100644 --- a/src/OppositeExpectation.php +++ b/src/OppositeExpectation.php @@ -32,7 +32,7 @@ final class OppositeExpectation foreach ($keys as $key) { try { $this->original->toHaveKey($key); - } catch (ExpectationFailedException) { + } catch (ExpectationFailedException $exception) { continue; } @@ -54,7 +54,7 @@ final class OppositeExpectation try { /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); - } catch (ExpectationFailedException) { + } catch (ExpectationFailedException $exception) { return $this->original; } @@ -70,7 +70,7 @@ final class OppositeExpectation { try { $this->original->{$name}; // @phpstan-ignore-line - } catch (ExpectationFailedException) { // @phpstan-ignore-line + } catch (ExpectationFailedException $exception) { // @phpstan-ignore-line return $this->original; } From 0b5cea6df1872a1944e2246298d04bcba3c5fd4e Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 18 Nov 2021 23:27:37 +0100 Subject: [PATCH 26/45] upgrade to phpstan lvl 9 --- phpstan.neon | 2 +- src/Datasets.php | 9 ++- src/Exceptions/ExpectationException.php | 7 ++- src/Expectation.php | 60 ++++++++++++++++++-- src/Support/Container.php | 2 +- src/Support/HigherOrderMessageCollection.php | 1 + 6 files changed, 70 insertions(+), 11 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 362ba447..ebd4bf30 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: 8 + level: 9 paths: - src diff --git a/src/Datasets.php b/src/Datasets.php index c91de5e7..7f8013fe 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -98,7 +98,8 @@ final class Datasets foreach ($datasetCombination as $dataset_data) { $partialDescriptions[] = $dataset_data['label']; - $values = array_merge($values, $dataset_data['values']); + //@phpstan-ignore-next-line + $values = array_merge($values, $dataset_data['values']); } $dataSetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions); @@ -152,10 +153,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, ]; } @@ -169,7 +171,7 @@ final class Datasets /** * @param array> $combinations * - * @return array> + * @return array>> */ private static function getDataSetsCombinations(array $combinations): array { @@ -184,6 +186,7 @@ final class Datasets $result = $tmp; } + //@phpstan-ignore-next-line return $result; } diff --git a/src/Exceptions/ExpectationException.php b/src/Exceptions/ExpectationException.php index ca14ce76..be68b40c 100644 --- a/src/Exceptions/ExpectationException.php +++ b/src/Exceptions/ExpectationException.php @@ -9,8 +9,13 @@ namespace Pest\Exceptions; */ final class ExpectationException extends \Exception { - public static function invalidValue(string $expectationName, string $valueRequired): ExpectationException + public static function invalidCurrentValueType(string $expectationName, string $valueRequired): ExpectationException { return new ExpectationException(sprintf('%s expectation requires a %s value.', $expectationName, $valueRequired)); } + + public static function invalidExpectedValueType(string $expectationName, string $valueRequired): ExpectationException + { + return new ExpectationException(sprintf('%s expectation requires a %s as expected value.', $expectationName, $valueRequired)); + } } diff --git a/src/Expectation.php b/src/Expectation.php index dada9447..5ee04b0b 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -70,7 +70,7 @@ final class Expectation public function json(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidValue('json', 'string'); + throw ExpectationException::invalidCurrentValueType('json', 'string'); } return $this->toBeJson()->and(json_decode($this->value, true)); @@ -360,8 +360,14 @@ final class Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { + if (!is_string($needle)) { + throw ExpectationException::invalidExpectedValueType('toContain', 'string'); + } Assert::assertStringContainsString($needle, $this->value); } else { + if (!is_iterable($this->value)) { + throw ExpectationException::invalidCurrentValueType('toContain', 'iterable'); + } Assert::assertContains($needle, $this->value); } } @@ -376,6 +382,10 @@ final class Expectation */ public function toStartWith(string $expected): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toStartWith', 'string'); + } + Assert::assertStringStartsWith($expected, $this->value); return $this; @@ -388,6 +398,10 @@ final class Expectation */ public function toEndWith(string $expected): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toEndWith', 'string'); + } + Assert::assertStringEndsWith($expected, $this->value); return $this; @@ -428,6 +442,10 @@ final class Expectation */ public function toHaveCount(int $count): Expectation { + if (!is_countable($this->value) && !is_iterable($this->value)) { + throw ExpectationException::invalidCurrentValueType('toHaveCount', 'string'); + } + Assert::assertCount($count, $this->value); return $this; @@ -440,6 +458,7 @@ final class Expectation { $this->toBeObject(); + //@phpstan-ignore-next-line Assert::assertTrue(property_exists($this->value, $name)); if (func_num_args() > 1) { @@ -651,6 +670,8 @@ final class Expectation public function toBeJson(): Expectation { Assert::assertIsString($this->value); + + //@phpstan-ignore-next-line Assert::assertJson($this->value); return $this; @@ -721,6 +742,10 @@ final class Expectation */ public function toBeDirectory(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toBeDirectory', 'string'); + } + Assert::assertDirectoryExists($this->value); return $this; @@ -731,6 +756,10 @@ final class Expectation */ public function toBeReadableDirectory(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toBeReadableDirectory', 'string'); + } + Assert::assertDirectoryIsReadable($this->value); return $this; @@ -741,6 +770,10 @@ final class Expectation */ public function toBeWritableDirectory(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toBeWritableDirectory', 'string'); + } + Assert::assertDirectoryIsWritable($this->value); return $this; @@ -751,6 +784,10 @@ final class Expectation */ public function toBeFile(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toBeFile', 'string'); + } + Assert::assertFileExists($this->value); return $this; @@ -761,6 +798,9 @@ final class Expectation */ public function toBeReadableFile(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toBeReadableFile', 'string'); + } Assert::assertFileIsReadable($this->value); return $this; @@ -771,6 +811,9 @@ final class Expectation */ public function toBeWritableFile(): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toBeWritableFile', 'string'); + } Assert::assertFileIsWritable($this->value); return $this; @@ -815,6 +858,9 @@ final class Expectation public function toMatchObject(iterable|object $object): Expectation { foreach ((array) $object as $property => $value) { + if (!is_object($this->value) && !is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toMatchObject', 'object|string'); + } Assert::assertTrue(property_exists($this->value, $property)); /* @phpstan-ignore-next-line */ @@ -838,6 +884,9 @@ final class Expectation */ public function toMatch(string $expression): Expectation { + if (!is_string($this->value)) { + throw ExpectationException::invalidCurrentValueType('toMatch', 'string'); + } Assert::assertMatchesRegularExpression($expression, $this->value); return $this; @@ -897,10 +946,10 @@ final class Expectation } if (!class_exists($exception)) { - throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); + throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); } - throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); + throw new ExpectationFailedException("Exception \"$exception\" not thrown."); } /** @@ -925,7 +974,7 @@ final class Expectation */ public function __call(string $method, array $parameters) { - if (!static::hasExtend($method)) { + if (!Expectation::hasExtend($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } @@ -939,7 +988,8 @@ final class Expectation */ public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation { - if (!method_exists($this, $name) && !static::hasExtend($name)) { + if (!method_exists($this, $name) && !Expectation::hasExtend($name)) { + //@phpstan-ignore-next-line return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } diff --git a/src/Support/Container.php b/src/Support/Container.php index 57b82f54..1f6b45cd 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -37,7 +37,7 @@ final class Container * * @param class-string $id * - * @return object + * @return mixed */ public function get(string $id) { diff --git a/src/Support/HigherOrderMessageCollection.php b/src/Support/HigherOrderMessageCollection.php index cb69a3a9..54bef82a 100644 --- a/src/Support/HigherOrderMessageCollection.php +++ b/src/Support/HigherOrderMessageCollection.php @@ -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; } } From 86dca12c0989a2d781822849ad6dd83f841f5922 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Thu, 18 Nov 2021 23:39:37 +0000 Subject: [PATCH 27/45] refacto(phpstan-to-8): few adjustments --- phpstan.neon | 2 +- src/Bootstrappers/BootSubscribers.php | 4 +- src/Datasets.php | 49 ++++++++++++---------- src/Exceptions/ExpectationException.php | 21 ---------- src/Exceptions/InvalidExpectationValue.php | 23 ++++++++++ src/Expectation.php | 36 ++++++++-------- src/Factories/Annotations/Depends.php | 6 +-- src/Factories/Annotations/Groups.php | 6 +-- src/Factories/TestCaseFactory.php | 10 ++--- src/Factories/TestCaseMethodFactory.php | 2 +- src/Functions.php | 2 +- src/PendingCalls/UsesCall.php | 6 +-- src/Plugins/Coverage.php | 6 +-- src/Support/Arr.php | 4 +- src/Support/ChainableClosure.php | 8 ++-- src/Support/Closure.php | 12 +++++- src/Support/ExceptionTrace.php | 2 +- src/Support/HigherOrderCallables.php | 2 +- 18 files changed, 107 insertions(+), 94 deletions(-) delete mode 100644 src/Exceptions/ExpectationException.php create mode 100644 src/Exceptions/InvalidExpectationValue.php diff --git a/phpstan.neon b/phpstan.neon index ebd4bf30..d5dc4ef6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: 9 + level: max paths: - src diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index 42bdeaba..12486f50 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -15,7 +15,7 @@ final class BootSubscribers /** * The Kernel subscribers. * - * @var array + * @var array> */ private static array $subscribers = [ Subscribers\EnsureConfigurationIsValid::class, @@ -29,7 +29,7 @@ final class BootSubscribers { foreach (self::$subscribers as $subscriber) { Event\Facade::registerSubscriber( - new $subscriber() //@phpstan-ignore-line + new $subscriber() ); } } diff --git a/src/Datasets.php b/src/Datasets.php index 7f8013fe..248b1d38 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -34,7 +34,7 @@ final class Datasets /** * Sets the given. * - * @phpstan-param Closure|iterable $data + * @param Closure|iterable $data */ public static function set(string $name, Closure|iterable $data): void { @@ -46,9 +46,9 @@ final class Datasets } /** - * Sets the given. + * Sets the given "with". * - * @phpstan-param array|string> $with + * @param array|string> $with */ public static function with(string $filename, string $description, array $with): void { @@ -56,7 +56,9 @@ final class Datasets } /** - * @return Closure|iterable + * @return Closure|iterable|never + * + * @throws ShouldNotHappen */ public static function get(string $filename, string $description): Closure|iterable { @@ -65,7 +67,7 @@ final class Datasets $dataset = self::resolve($description, $dataset); if ($dataset === null) { - throw ShouldNotHappen::fromMessage('Could not resolve dataset.'); + throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.'); } return $dataset; @@ -87,39 +89,40 @@ final class Datasets $dataset = self::processDatasets($dataset); - $datasetCombinations = self::getDataSetsCombinations($dataset); + $datasetCombinations = self::getDatasetsCombinations($dataset); - $dataSetDescriptions = []; - $dataSetValues = []; + $datasetDescriptions = []; + $datasetValues = []; foreach ($datasetCombinations as $datasetCombination) { $partialDescriptions = []; $values = []; - foreach ($datasetCombination as $dataset_data) { - $partialDescriptions[] = $dataset_data['label']; - //@phpstan-ignore-next-line - $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; @@ -157,7 +160,7 @@ final class Datasets foreach ($datasets[$index] as $key => $values) { $values = is_array($values) ? $values : [$values]; $processedDataset[] = [ - 'label' => self::getDataSetDescription($key, $values), //@phpstan-ignore-line + 'label' => self::getDatasetDescription($key, $values), //@phpstan-ignore-line 'values' => $values, ]; } @@ -173,7 +176,7 @@ final class Datasets * * @return array>> */ - private static function getDataSetsCombinations(array $combinations): array + private static function getDatasetsCombinations(array $combinations): array { $result = [[]]; foreach ($combinations as $index => $values) { @@ -193,7 +196,7 @@ final class Datasets /** * @param array $data */ - private static function getDataSetDescription(int|string $key, array $data): string + private static function getDatasetDescription(int|string $key, array $data): string { $exporter = new Exporter(); diff --git a/src/Exceptions/ExpectationException.php b/src/Exceptions/ExpectationException.php deleted file mode 100644 index be68b40c..00000000 --- a/src/Exceptions/ExpectationException.php +++ /dev/null @@ -1,21 +0,0 @@ -value)) { - throw ExpectationException::invalidCurrentValueType('json', 'string'); + InvalidExpectationValue::expected('string'); } return $this->toBeJson()->and(json_decode($this->value, true)); @@ -360,13 +360,11 @@ final class Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { - if (!is_string($needle)) { - throw ExpectationException::invalidExpectedValueType('toContain', 'string'); - } - Assert::assertStringContainsString($needle, $this->value); + // @phpstan-ignore-next-line + Assert::assertStringContainsString((string) $needle, $this->value); } else { if (!is_iterable($this->value)) { - throw ExpectationException::invalidCurrentValueType('toContain', 'iterable'); + InvalidExpectationValue::expected('iterable'); } Assert::assertContains($needle, $this->value); } @@ -383,7 +381,7 @@ final class Expectation public function toStartWith(string $expected): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toStartWith', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertStringStartsWith($expected, $this->value); @@ -399,7 +397,7 @@ final class Expectation public function toEndWith(string $expected): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toEndWith', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertStringEndsWith($expected, $this->value); @@ -443,7 +441,7 @@ final class Expectation public function toHaveCount(int $count): Expectation { if (!is_countable($this->value) && !is_iterable($this->value)) { - throw ExpectationException::invalidCurrentValueType('toHaveCount', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertCount($count, $this->value); @@ -743,7 +741,7 @@ final class Expectation public function toBeDirectory(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toBeDirectory', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertDirectoryExists($this->value); @@ -757,7 +755,7 @@ final class Expectation public function toBeReadableDirectory(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toBeReadableDirectory', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertDirectoryIsReadable($this->value); @@ -771,7 +769,7 @@ final class Expectation public function toBeWritableDirectory(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toBeWritableDirectory', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertDirectoryIsWritable($this->value); @@ -785,7 +783,7 @@ final class Expectation public function toBeFile(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toBeFile', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertFileExists($this->value); @@ -799,8 +797,9 @@ final class Expectation public function toBeReadableFile(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toBeReadableFile', 'string'); + InvalidExpectationValue::expected('string'); } + Assert::assertFileIsReadable($this->value); return $this; @@ -812,7 +811,7 @@ final class Expectation public function toBeWritableFile(): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toBeWritableFile', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertFileIsWritable($this->value); @@ -859,8 +858,9 @@ final class Expectation { foreach ((array) $object as $property => $value) { if (!is_object($this->value) && !is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toMatchObject', 'object|string'); + InvalidExpectationValue::expected('object|string'); } + Assert::assertTrue(property_exists($this->value, $property)); /* @phpstan-ignore-next-line */ @@ -885,7 +885,7 @@ final class Expectation public function toMatch(string $expression): Expectation { if (!is_string($this->value)) { - throw ExpectationException::invalidCurrentValueType('toMatch', 'string'); + InvalidExpectationValue::expected('string'); } Assert::assertMatchesRegularExpression($expression, $this->value); diff --git a/src/Factories/Annotations/Depends.php b/src/Factories/Annotations/Depends.php index 66c4838c..f41076d9 100644 --- a/src/Factories/Annotations/Depends.php +++ b/src/Factories/Annotations/Depends.php @@ -15,11 +15,11 @@ final class Depends /** * Adds annotations regarding the "depends" feature. * - * @param array $annotations + * @param array $annotations * - * @return array + * @return array */ - public function add(TestCaseMethodFactory $method, array $annotations): array + public function __invoke(TestCaseMethodFactory $method, array $annotations): array { foreach ($method->depends as $depend) { $depend = Str::evaluable($depend); diff --git a/src/Factories/Annotations/Groups.php b/src/Factories/Annotations/Groups.php index ab7749b7..0876ff5c 100644 --- a/src/Factories/Annotations/Groups.php +++ b/src/Factories/Annotations/Groups.php @@ -14,11 +14,11 @@ final class Groups /** * Adds annotations regarding the "groups" feature. * - * @param array $annotations + * @param array $annotations * - * @return array + * @return array */ - public function add(TestCaseMethodFactory $method, array $annotations): array + public function __invoke(TestCaseMethodFactory $method, array $annotations): array { foreach ($method->groups as $group) { $annotations[] = "@group $group"; diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 517a586c..f6a61ba2 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -73,9 +73,9 @@ final class TestCaseFactory { $methodsUsingOnly = $this->methodsUsingOnly(); - $methods = array_filter($this->methods, function ($method) use ($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); @@ -99,7 +99,7 @@ final class TestCaseFactory /** * Creates a Test Case class using a runtime evaluate. * - * @param array $methods + * @param array $methods */ public function evaluate(string $filename, array $methods): string { @@ -152,8 +152,8 @@ final class TestCaseFactory $annotations = ['@test']; foreach (self::$annotations as $annotation) { - //@phpstan-ignore-next-line - $annotations = (new $annotation())->add($method, $annotations); + /** @phpstan-ignore-next-line */ + $annotations = (new $annotation())->__invoke($method, $annotations); } if (count($method->datasets) > 0) { diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index e2ece273..540f2677 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -89,7 +89,7 @@ final class TestCaseMethodFactory $testCase->chains->chain($this); $method->chains->chain($this); - return \Pest\Support\Closure::safeBind($closure, $this, $this::class)(...func_get_args()); + return \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); }; } diff --git a/src/Functions.php b/src/Functions.php index 0f72cee7..fe1dd39a 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -77,7 +77,7 @@ if (!function_exists('uses')) { { $filename = Backtrace::file(); - return new UsesCall($filename, $classAndTraits); + return new UsesCall($filename, array_values($classAndTraits)); } } diff --git a/src/PendingCalls/UsesCall.php b/src/PendingCalls/UsesCall.php index 457f8699..cb3fdc8c 100644 --- a/src/PendingCalls/UsesCall.php +++ b/src/PendingCalls/UsesCall.php @@ -36,14 +36,14 @@ final class UsesCall /** * Holds the groups of the uses. * - * @var array + * @var array */ private array $groups = []; /** * Creates a new Pending Call. * - * @param array $classAndTraits + * @param array $classAndTraits */ public function __construct( private string $filename, @@ -89,7 +89,7 @@ final class UsesCall */ public function group(string ...$groups): UsesCall { - $this->groups = $groups; + $this->groups = array_values($groups); return $this; } diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index f2ee5457..9cf45f3b 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -80,10 +80,10 @@ final class Coverage implements AddsOutput, HandlesArguments } if ($input->getOption(self::MIN_OPTION) !== null) { - /** @var int|float $min_option */ - $min_option = $input->getOption(self::MIN_OPTION); + /** @var int|float $minOption */ + $minOption = $input->getOption(self::MIN_OPTION); - $this->coverageMin = (float) $min_option; + $this->coverageMin = (float) $minOption; } return $originals; diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 531e0d19..922110e1 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -12,7 +12,7 @@ final class Arr /** * Checks if the given array has the given key. * - * @param array $array + * @param array $array */ public static function has(array $array, string|int $key): bool { @@ -36,7 +36,7 @@ final class Arr /** * Gets the given key value. * - * @param array $array + * @param array $array */ public static function get(array $array, string|int $key, mixed $default = null): mixed { diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index b3406d86..5c92e6fd 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -22,8 +22,8 @@ final class ChainableClosure throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); } - \Pest\Support\Closure::safeBind($closure, $this, $this::class)(...func_get_args()); - \Pest\Support\Closure::safeBind($next, $this, $this::class)(...func_get_args()); + \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); + \Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args()); }; } @@ -33,8 +33,8 @@ final class ChainableClosure public static function fromStatic(Closure $closure, Closure $next): Closure { return static function () use ($closure, $next): void { - \Pest\Support\Closure::safeBind($closure, null, self::class)(...func_get_args()); - \Pest\Support\Closure::safeBind($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()); }; } } diff --git a/src/Support/Closure.php b/src/Support/Closure.php index ece77d57..decb9b71 100644 --- a/src/Support/Closure.php +++ b/src/Support/Closure.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Pest\Support; +use Closure as BaseClosure; use Pest\Exceptions\ShouldNotHappen; /** @@ -11,13 +12,20 @@ use Pest\Exceptions\ShouldNotHappen; */ final class Closure { - public static function safeBind(\Closure|null $closure, ?object $newThis, object|string|null $newScope = 'static'): \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 = \Closure::bind($closure, $newThis, $newScope); + $closure = BaseClosure::bind($closure, $newThis, $newScope); if ($closure == false) { throw ShouldNotHappen::fromMessage('Could not bind closure.'); diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index 17fc2e40..2e6d53f4 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -51,7 +51,7 @@ final class ExceptionTrace $property = new ReflectionProperty($t, 'serializableTrace'); $property->setAccessible(true); - /** @var array> $trace */ + /** @var array> $trace */ $trace = $property->getValue($t); $cleanedTrace = []; diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php index be02b86d..c6533929 100644 --- a/src/Support/HigherOrderCallables.php +++ b/src/Support/HigherOrderCallables.php @@ -25,7 +25,7 @@ final class HigherOrderCallables * * Create a new expectation. Callable values will be executed prior to returning the new expectation. * - * @param (callable():TValue)|TValue $value + * @param (Closure():TValue)|TValue $value * * @return Expectation */ From 47264416b1eff0a64cfed64b054da7066c517489 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 21 Nov 2021 18:25:07 +0000 Subject: [PATCH 28/45] Update FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8e345cd7..07e7fdea 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -2,3 +2,4 @@ github: [nunomaduro,owenvoke,olivernybroe,octoper,lukeraymonddowning] patreon: nunomaduro +custom: https://www.paypal.com/paypalme/enunomaduro From fce24ef01fb5162b05d64610be31de33733b4b71 Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Thu, 25 Nov 2021 16:49:10 +0000 Subject: [PATCH 29/45] Port from 1.x --- src/Expectation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Expectation.php b/src/Expectation.php index 2dea2cb0..54ad80e2 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -160,7 +160,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; } From cdd67a690004c9cbcb1d2ef9e1fbc67cdb10d5c3 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 26 Nov 2021 15:20:35 +0100 Subject: [PATCH 30/45] merge from master --- src/CoreExpectation.php | 81 ++- src/Expectation.php | 799 +++------------------------- src/Support/ExpectationPipeline.php | 25 +- 3 files changed, 132 insertions(+), 773 deletions(-) diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php index cd4a9099..bb7c229e 100644 --- a/src/CoreExpectation.php +++ b/src/CoreExpectation.php @@ -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; @@ -29,30 +30,22 @@ final class CoreExpectation { use RetrievesValues; - /** - * The expectation value. - * - * @readonly - * - * @var TValue - */ - public mixed $value; - /** * The exporter instance, if any. * * @readonly */ - private Exporter|null $exporter; + private Exporter|null $exporter = null; /** * Creates a new expectation. * * @param TValue $value */ - public function __construct(mixed $value) - { - $this->value = $value; + public function __construct( + public mixed $value + ) { + // .. } /** @@ -164,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); } } @@ -180,6 +177,10 @@ final class CoreExpectation */ public function toStartWith(string $expected): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertStringStartsWith($expected, $this->value); return $this; @@ -192,6 +193,10 @@ final class CoreExpectation */ public function toEndWith(string $expected): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertStringEndsWith($expected, $this->value); return $this; @@ -224,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.'); } /** @@ -232,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; @@ -244,6 +253,7 @@ final class CoreExpectation { $this->toBeObject(); + //@phpstan-ignore-next-line Assert::assertTrue(property_exists($this->value, $name)); if (func_num_args() > 1) { @@ -455,6 +465,8 @@ final class CoreExpectation public function toBeJson(): CoreExpectation { Assert::assertIsString($this->value); + + //@phpstan-ignore-next-line Assert::assertJson($this->value); return $this; @@ -493,6 +505,8 @@ final class CoreExpectation try { Assert::assertTrue(Arr::has($array, $key)); + + /* @phpstan-ignore-next-line */ } catch (ExpectationFailedException $exception) { throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); } @@ -523,6 +537,10 @@ final class CoreExpectation */ public function toBeDirectory(): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertDirectoryExists($this->value); return $this; @@ -533,6 +551,10 @@ final class CoreExpectation */ public function toBeReadableDirectory(): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertDirectoryIsReadable($this->value); return $this; @@ -543,6 +565,10 @@ final class CoreExpectation */ public function toBeWritableDirectory(): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertDirectoryIsWritable($this->value); return $this; @@ -553,6 +579,10 @@ final class CoreExpectation */ public function toBeFile(): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertFileExists($this->value); return $this; @@ -563,6 +593,10 @@ final class CoreExpectation */ public function toBeReadableFile(): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertFileIsReadable($this->value); return $this; @@ -573,6 +607,9 @@ final class CoreExpectation */ public function toBeWritableFile(): CoreExpectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } Assert::assertFileIsWritable($this->value); return $this; @@ -581,7 +618,7 @@ final class CoreExpectation /** * Asserts that the value array matches the given array subset. * - * @phpstan-param iterable $array + * @param iterable $array */ public function toMatchArray(iterable|object $array): CoreExpectation { @@ -612,11 +649,15 @@ final class CoreExpectation * Asserts that the value object matches a subset * of the properties of an given object. * - * @phpstan-param iterable|object $object + * @param iterable|object $object */ 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 */ @@ -640,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; @@ -650,7 +694,6 @@ final class CoreExpectation */ public function toMatchConstraint(Constraint $constraint): CoreExpectation { - Assert::assertThat($this->value, $constraint); return $this; @@ -659,7 +702,7 @@ final class CoreExpectation /** * Asserts that executing value throws an exception. * - * @phpstan-param (Closure(Throwable): mixed)|string $exception + * @param (Closure(Throwable): mixed)|string $exception */ public function toThrow(callable|string $exception, string $exceptionMessage = null): CoreExpectation { diff --git a/src/Expectation.php b/src/Expectation.php index 54ad80e2..3310451c 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -6,19 +6,13 @@ namespace Pest; use BadMethodCallException; use Closure; -use InvalidArgumentException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; use Pest\Exceptions\InvalidExpectationValue; -use Pest\Support\Arr; -use Pest\Support\NullClosure; +use Pest\Exceptions\PipeException; +use Pest\Support\ExpectationPipeline; use PHPUnit\Framework\Assert; -use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; -use ReflectionFunction; -use ReflectionNamedType; -use SebastianBergmann\Exporter\Exporter; -use Throwable; /** * @internal @@ -27,6 +21,8 @@ use Throwable; * * @property Expectation $not Creates the opposite expectation. * @property Each $each Creates an expectation on each element on the traversable value. + * + * @mixin CoreExpectation */ final class Expectation { @@ -34,22 +30,16 @@ final class Expectation __call as __extendsCall; } - /** - * The exporter instance, if any. - * - * @readonly - */ - private ?Exporter $exporter = null; + private CoreExpectation $coreExpectation; /** * Creates a new expectation. * * @param TValue $value */ - public function __construct( - public mixed $value - ) { - // .. + public function __construct(mixed $value) + { + $this->coreExpectation = new CoreExpectation($value); } /** @@ -251,749 +241,80 @@ final class Expectation return $this; } - /** - * Asserts that two variables have the same type and - * value. Used on objects, it asserts that two - * variables reference the same object. - */ - public function toBe(mixed $expected): Expectation - { - Assert::assertSame($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is empty. - */ - public function toBeEmpty(): Expectation - { - Assert::assertEmpty($this->value); - - return $this; - } - - /** - * Asserts that the value is true. - */ - public function toBeTrue(): Expectation - { - Assert::assertTrue($this->value); - - return $this; - } - - /** - * Asserts that the value is truthy. - */ - public function toBeTruthy(): Expectation - { - Assert::assertTrue((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is false. - */ - public function toBeFalse(): Expectation - { - Assert::assertFalse($this->value); - - return $this; - } - - /** - * Asserts that the value is falsy. - */ - public function toBeFalsy(): Expectation - { - Assert::assertFalse((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than $expected. - */ - public function toBeGreaterThan(int|float $expected): Expectation - { - Assert::assertGreaterThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than or equal to $expected. - */ - public function toBeGreaterThanOrEqual(int|float $expected): Expectation - { - Assert::assertGreaterThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than or equal to $expected. - */ - public function toBeLessThan(int|float $expected): Expectation - { - Assert::assertLessThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than $expected. - */ - public function toBeLessThanOrEqual(int|float $expected): Expectation - { - Assert::assertLessThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that $needle is an element of the value. - */ - public function toContain(mixed ...$needles): Expectation - { - foreach ($needles as $needle) { - if (is_string($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); - } - } - - return $this; - } - - /** - * Asserts that the value starts with $expected. - * - * @param non-empty-string $expected - */ - public function toStartWith(string $expected): Expectation - { - 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): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertStringEndsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that $number matches value's Length. - */ - public function toHaveLength(int $number): Expectation - { - if (is_string($this->value)) { - Assert::assertEquals($number, mb_strlen($this->value)); - - return $this; - } - - if (is_iterable($this->value)) { - return $this->toHaveCount($number); - } - - if (is_object($this->value)) { - if (method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - Assert::assertCount($number, $array); - - return $this; - } - - throw new BadMethodCallException('Expectation value length is not countable.'); - } - - /** - * Asserts that $count matches the number of elements of the value. - */ - public function toHaveCount(int $count): Expectation - { - if (!is_countable($this->value) && !is_iterable($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertCount($count, $this->value); - - return $this; - } - - /** - * Asserts that the value contains the property $name. - */ - public function toHaveProperty(string $name, mixed $value = null): Expectation - { - $this->toBeObject(); - - //@phpstan-ignore-next-line - Assert::assertTrue(property_exists($this->value, $name)); - - if (func_num_args() > 1) { - /* @phpstan-ignore-next-line */ - Assert::assertEquals($value, $this->value->{$name}); - } - - return $this; - } - - /** - * Asserts that the value contains the provided properties $names. - * - * @param iterable $names - */ - public function toHaveProperties(iterable $names): Expectation - { - foreach ($names as $name) { - $this->toHaveProperty($name); - } - - return $this; - } - - /** - * Asserts that two variables have the same value. - */ - public function toEqual(mixed $expected): Expectation - { - Assert::assertEquals($expected, $this->value); - - return $this; - } - - /** - * Asserts that two variables have the same value. - * The contents of $expected and the $this->value are - * canonicalized before they are compared. For instance, when the two - * variables $expected and $this->value are arrays, then these arrays - * are sorted before they are compared. When $expected and $this->value - * are objects, each object is converted to an array containing all - * private, protected and public attributes. - */ - public function toEqualCanonicalizing(mixed $expected): Expectation - { - Assert::assertEqualsCanonicalizing($expected, $this->value); - - return $this; - } - - /** - * Asserts that the absolute difference between the value and $expected - * is lower than $delta. - */ - public function toEqualWithDelta(mixed $expected, float $delta): Expectation - { - Assert::assertEqualsWithDelta($expected, $this->value, $delta); - - return $this; - } - - /** - * Asserts that the value is one of the given values. - * - * @param iterable $values - */ - public function toBeIn(iterable $values): Expectation - { - Assert::assertContains($this->value, $values); - - return $this; - } - - /** - * Asserts that the value is infinite. - */ - public function toBeInfinite(): Expectation - { - Assert::assertInfinite($this->value); - - return $this; - } - - /** - * Asserts that the value is an instance of $class. - * - * @param class-string $class - */ - public function toBeInstanceOf(string $class): Expectation - { - Assert::assertInstanceOf($class, $this->value); - - return $this; - } - - /** - * Asserts that the value is an array. - */ - public function toBeArray(): Expectation - { - Assert::assertIsArray($this->value); - - return $this; - } - - /** - * Asserts that the value is of type bool. - */ - public function toBeBool(): Expectation - { - Assert::assertIsBool($this->value); - - return $this; - } - - /** - * Asserts that the value is of type callable. - */ - public function toBeCallable(): Expectation - { - Assert::assertIsCallable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type float. - */ - public function toBeFloat(): Expectation - { - Assert::assertIsFloat($this->value); - - return $this; - } - - /** - * Asserts that the value is of type int. - */ - public function toBeInt(): Expectation - { - Assert::assertIsInt($this->value); - - return $this; - } - - /** - * Asserts that the value is of type iterable. - */ - public function toBeIterable(): Expectation - { - Assert::assertIsIterable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type numeric. - */ - public function toBeNumeric(): Expectation - { - Assert::assertIsNumeric($this->value); - - return $this; - } - - /** - * Asserts that the value is of type object. - */ - public function toBeObject(): Expectation - { - Assert::assertIsObject($this->value); - - return $this; - } - - /** - * Asserts that the value is of type resource. - */ - public function toBeResource(): Expectation - { - Assert::assertIsResource($this->value); - - return $this; - } - - /** - * Asserts that the value is of type scalar. - */ - public function toBeScalar(): Expectation - { - Assert::assertIsScalar($this->value); - - return $this; - } - - /** - * Asserts that the value is of type string. - */ - public function toBeString(): Expectation - { - Assert::assertIsString($this->value); - - return $this; - } - - /** - * Asserts that the value is a JSON string. - */ - public function toBeJson(): Expectation - { - Assert::assertIsString($this->value); - - //@phpstan-ignore-next-line - Assert::assertJson($this->value); - - return $this; - } - - /** - * Asserts that the value is NAN. - */ - public function toBeNan(): Expectation - { - Assert::assertNan($this->value); - - return $this; - } - - /** - * Asserts that the value is null. - */ - public function toBeNull(): Expectation - { - Assert::assertNull($this->value); - - return $this; - } - - /** - * Asserts that the value array has the provided $key. - */ - public function toHaveKey(string|int $key, mixed $value = null): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - try { - Assert::assertTrue(Arr::has($array, $key)); - - /* @phpstan-ignore-next-line */ - } catch (ExpectationFailedException $exception) { - throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); - } - - if (func_num_args() > 1) { - Assert::assertEquals($value, Arr::get($array, $key)); - } - - return $this; - } - - /** - * Asserts that the value array has the provided $keys. - * - * @param array $keys - */ - public function toHaveKeys(array $keys): Expectation - { - foreach ($keys as $key) { - $this->toHaveKey($key); - } - - return $this; - } - - /** - * Asserts that the value is a directory. - */ - public function toBeDirectory(): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertDirectoryExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is readable. - */ - public function toBeReadableDirectory(): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertDirectoryIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is writable. - */ - public function toBeWritableDirectory(): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertDirectoryIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file. - */ - public function toBeFile(): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertFileExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is readable. - */ - public function toBeReadableFile(): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - - Assert::assertFileIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is writable. - */ - public function toBeWritableFile(): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - Assert::assertFileIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value array matches the given array subset. - * - * @param iterable $array - */ - public function toMatchArray(iterable|object $array): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $valueAsArray = $this->value->toArray(); - } else { - $valueAsArray = (array) $this->value; - } - - foreach ($array as $key => $value) { - Assert::assertArrayHasKey($key, $valueAsArray); - - Assert::assertEquals( - $value, - $valueAsArray[$key], - sprintf( - 'Failed asserting that an array has a key %s with the value %s.', - $this->export($key), - $this->export($valueAsArray[$key]), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value object matches a subset - * of the properties of an given object. - * - * @param iterable|object $object - */ - public function toMatchObject(iterable|object $object): Expectation - { - 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 */ - $propertyValue = $this->value->{$property}; - Assert::assertEquals( - $value, - $propertyValue, - sprintf( - 'Failed asserting that an object has a property %s with the value %s.', - $this->export($property), - $this->export($propertyValue), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value matches a regular expression. - */ - public function toMatch(string $expression): Expectation - { - if (!is_string($this->value)) { - InvalidExpectationValue::expected('string'); - } - Assert::assertMatchesRegularExpression($expression, $this->value); - - return $this; - } - - /** - * Asserts that the value matches a constraint. - */ - public function toMatchConstraint(Constraint $constraint): Expectation - { - Assert::assertThat($this->value, $constraint); - - return $this; - } - - /** - * Asserts that executing value throws an exception. - * - * @param (Closure(Throwable): mixed)|string $exception - */ - public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation - { - $callback = NullClosure::create(); - - if ($exception instanceof Closure) { - $callback = $exception; - $parameters = (new ReflectionFunction($exception))->getParameters(); - - if (1 !== count($parameters)) { - throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); - } - - if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { - throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); - } - - $exception = $type->getName(); - } - - try { - ($this->value)(); - } catch (Throwable $e) { // @phpstan-ignore-line - if (!class_exists($exception)) { - Assert::assertStringContainsString($exception, $e->getMessage()); - - return $this; - } - - if ($exceptionMessage !== null) { - Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); - } - - Assert::assertInstanceOf($exception, $e); - $callback($e); - - return $this; - } - - if (!class_exists($exception)) { - throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); - } - - throw new ExpectationFailedException("Exception \"$exception\" not thrown."); - } - - /** - * Exports the given value. - */ - private function export(mixed $value): string - { - if ($this->exporter === null) { - $this->exporter = new Exporter(); - } - - return $this->exporter->export($value); - } - /** * Dynamically handle calls to the class or * creates a new higher order expectation. * * @param array $parameters - * - * @return HigherOrderExpectation|mixed */ - public function __call(string $method, array $parameters) + public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation { - if (!Expectation::hasExtend($method)) { + if (!$this->hasExpectation($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } - return $this->__extendsCall($method, $parameters); + ExpectationPipeline::for($this->getExpectationClosure($method)) + ->send(...$parameters) + ->through($this->pipes($method, $this, Expectation::class)) + ->run(); + + return $this; + } + + private function getExpectationClosure(string $name): Closure + { + if (method_exists($this->coreExpectation, $name)) { + //@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 PipeException::expectationNotFound($name); + } + + private function hasExpectation(string $name): bool + { + if (method_exists($this->coreExpectation, $name)) { + return true; + } + + if (self::hasExtend($name)) { + return true; + } + + return false; } /** * 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): Expectation|OppositeExpectation|Each|HigherOrderExpectation + public function __get(string $name) { - if (!method_exists($this, $name) && !Expectation::hasExtend($name)) { - //@phpstan-ignore-next-line + 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 $this->{$name}(); } + + public static function hasMethod(string $name): bool + { + return method_exists(CoreExpectation::class, $name); + } } diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php index 9ebba221..0ddc9260 100644 --- a/src/Support/ExpectationPipeline.php +++ b/src/Support/ExpectationPipeline.php @@ -6,35 +6,30 @@ namespace Pest\Support; use Closure; +/** + * @internal + */ final class ExpectationPipeline { /** @var array */ - private $pipes = []; + private array $pipes = []; /** @var array */ - private $passable; + private array $passable; - /** @var Closure */ - private $expectationClosure; + private Closure $expectationClosure; - /** @var string */ - private $expectationName; - - public function __construct(string $expectationName, Closure $expectationClosure) + public function __construct(Closure $expectationClosure) { $this->expectationClosure = $expectationClosure; - $this->expectationName = $expectationName; } - public static function for(string $expectationName, Closure $expectationClosure): self + public static function for(Closure $expectationClosure): self { - return new self($expectationName, $expectationClosure); + return new self($expectationClosure); } - /** - * @param array $passable - */ - public function send(...$passable): self + public function send(mixed ...$passable): self { $this->passable = $passable; From 5f0752e8745f194c3952bbc1c6cfef3132e79c90 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 26 Nov 2021 15:28:43 +0100 Subject: [PATCH 31/45] applied changes from code review --- src/Concerns/Extendable.php | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index e569aa47..8713de72 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -19,7 +19,7 @@ trait Extendable */ private static array $extends = []; - /** @var array> */ + /** @var array> */ private static array $pipes = []; /** @@ -49,9 +49,8 @@ trait Extendable }; } - //@phpstan-ignore-next-line self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) { - //@phpstan-ignore-next-line + /** @phpstan-ignore-next-line */ if ($filter($this->value)) { //@phpstan-ignore-next-line $handler->bindTo($this, get_class($this))(...$arguments); @@ -71,30 +70,12 @@ trait Extendable return array_key_exists($name, static::$extends); } - /** - * Checks if pipes are registered for a given expectation. - */ - public static function hasPipes(string $name): bool - { - return array_key_exists($name, static::$pipes); - } - /** * @return array */ - public function pipes(string $name, object $context, string $scope): array + private function pipes(string $name, object $context, string $scope): array { - if (!self::hasPipes($name)) { - return []; - } - - $decorators = []; - foreach (self::$pipes[$name] as $decorator) { - $decorators[] = $decorator->bindTo($context, $scope); - } - - //@phpstan-ignore-next-line - return $decorators; + return array_map(fn(Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } /** From 7fd9cfa2e9e33e168aca545be70a33893255228f Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 26 Nov 2021 15:44:35 +0100 Subject: [PATCH 32/45] refactor --- src/Concerns/Extendable.php | 6 +- tests/Features/Expect/pipe.php | 240 -------------------------------- tests/Features/Expect/pipes.php | 8 +- 3 files changed, 8 insertions(+), 246 deletions(-) delete mode 100644 tests/Features/Expect/pipe.php diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 8713de72..1a8dc379 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -19,7 +19,7 @@ trait Extendable */ private static array $extends = []; - /** @var array> */ + /** @var array> */ private static array $pipes = []; /** @@ -50,7 +50,7 @@ trait Extendable } self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) { - /** @phpstan-ignore-next-line */ + /* @phpstan-ignore-next-line */ if ($filter($this->value)) { //@phpstan-ignore-next-line $handler->bindTo($this, get_class($this))(...$arguments); @@ -75,7 +75,7 @@ trait Extendable */ private function pipes(string $name, object $context, string $scope): array { - return array_map(fn(Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); + return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); } /** diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php deleted file mode 100644 index e8574acf..00000000 --- a/tests/Features/Expect/pipe.php +++ /dev/null @@ -1,240 +0,0 @@ -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); -}); diff --git a/tests/Features/Expect/pipes.php b/tests/Features/Expect/pipes.php index c98d25fe..5e311e07 100644 --- a/tests/Features/Expect/pipes.php +++ b/tests/Features/Expect/pipes.php @@ -1,5 +1,7 @@ reset(); expect(3)->toBe(3) From 8494d4566a880d0bef850667215666e0cf2d42e5 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Fri, 26 Nov 2021 16:15:36 +0100 Subject: [PATCH 33/45] fix tests --- src/Concerns/Extendable.php | 6 ++++-- .../ExpectationNotFoundException.php | 18 ------------------ 2 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/Exceptions/ExpectationNotFoundException.php diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 1a8dc379..6f83693a 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -39,7 +39,9 @@ trait Extendable } /** - * Recister an interceptor that should replace an existing expectation. + * Register an interceptor that should replace an existing expectation. + * + * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter */ public static function intercept(string $name, string|Closure $filter, Closure $handler): void { @@ -51,7 +53,7 @@ trait Extendable self::pipe($name, function ($next, ...$arguments) use ($handler, $filter) { /* @phpstan-ignore-next-line */ - if ($filter($this->value)) { + if ($filter($this->value, ...$arguments)) { //@phpstan-ignore-next-line $handler->bindTo($this, get_class($this))(...$arguments); diff --git a/src/Exceptions/ExpectationNotFoundException.php b/src/Exceptions/ExpectationNotFoundException.php deleted file mode 100644 index 07bafdc3..00000000 --- a/src/Exceptions/ExpectationNotFoundException.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Sat, 27 Nov 2021 19:54:39 +0000 Subject: [PATCH 34/45] Updates from main --- src/Concerns/Testable.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 76e8bc88..e45f1956 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -7,6 +7,7 @@ namespace Pest\Concerns; use Closure; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; +use Pest\Support\Reflection; use Pest\TestSuite; use Throwable; @@ -210,7 +211,28 @@ trait Testable */ private function __resolveTestArguments(array $arguments): array { - return array_map(fn ($data) => $data instanceof Closure ? $this->__callClosure($data, []) : $data, $arguments); + if (count($arguments) !== 1) { + return $arguments; + } + + if (!$arguments[0] instanceof Closure) { + return $arguments; + } + + $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); + $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); + + if (in_array($testParameterTypes[0], ['Closure', 'callable'])) { + return $arguments; + } + + $boundDatasetResult = $this->__callClosure($arguments[0], []); + + if (count($testParameterTypes) === 1 || !is_array($boundDatasetResult)) { + return [$boundDatasetResult]; + } + + return array_values($boundDatasetResult); } /** From 337d55b9ab17daa0b38c0e6b189ed500c5f5d110 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 29 Nov 2021 09:25:00 +0100 Subject: [PATCH 35/45] lint --- src/Expectation.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Expectation.php b/src/Expectation.php index 5a118e00..3310451c 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -258,6 +258,7 @@ final class Expectation ->send(...$parameters) ->through($this->pipes($method, $this, Expectation::class)) ->run(); + return $this; } From b2eb69cbc10a2e4554f96356791e9b109ec7d5ac Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 29 Nov 2021 09:47:33 +0100 Subject: [PATCH 36/45] fix phpstan --- src/CoreExpectation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php index bb7c229e..c95e5505 100644 --- a/src/CoreExpectation.php +++ b/src/CoreExpectation.php @@ -24,7 +24,7 @@ use Throwable; * * @template TValue * - * @mixin Expectation + * @mixin Expectation */ final class CoreExpectation { From 2d2760e15c76139cbce6eb519f622802f1f1007e Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 29 Nov 2021 09:52:18 +0100 Subject: [PATCH 37/45] fix phpstan --- src/CoreExpectation.php | 100 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php index c95e5505..153a935d 100644 --- a/src/CoreExpectation.php +++ b/src/CoreExpectation.php @@ -52,6 +52,8 @@ final class CoreExpectation * Asserts that two variables have the same type and * value. Used on objects, it asserts that two * variables reference the same object. + * + * @return CoreExpectation */ public function toBe(mixed $expected): CoreExpectation { @@ -62,6 +64,8 @@ final class CoreExpectation /** * Asserts that the value is empty. + * + * @return CoreExpectation */ public function toBeEmpty(): CoreExpectation { @@ -72,6 +76,8 @@ final class CoreExpectation /** * Asserts that the value is true. + * + * @return CoreExpectation */ public function toBeTrue(): CoreExpectation { @@ -82,6 +88,8 @@ final class CoreExpectation /** * Asserts that the value is truthy. + * + * @return CoreExpectation */ public function toBeTruthy(): CoreExpectation { @@ -92,6 +100,8 @@ final class CoreExpectation /** * Asserts that the value is false. + * + * @return CoreExpectation */ public function toBeFalse(): CoreExpectation { @@ -102,6 +112,8 @@ final class CoreExpectation /** * Asserts that the value is falsy. + * + * @return CoreExpectation */ public function toBeFalsy(): CoreExpectation { @@ -112,6 +124,8 @@ final class CoreExpectation /** * Asserts that the value is greater than $expected. + * + * @return CoreExpectation */ public function toBeGreaterThan(int|float $expected): CoreExpectation { @@ -122,6 +136,8 @@ final class CoreExpectation /** * Asserts that the value is greater than or equal to $expected. + * + * @return CoreExpectation */ public function toBeGreaterThanOrEqual(int|float $expected): CoreExpectation { @@ -132,6 +148,8 @@ final class CoreExpectation /** * Asserts that the value is less than or equal to $expected. + * + * @return CoreExpectation */ public function toBeLessThan(int|float $expected): CoreExpectation { @@ -142,6 +160,8 @@ final class CoreExpectation /** * Asserts that the value is less than $expected. + * + * @return CoreExpectation */ public function toBeLessThanOrEqual(int|float $expected): CoreExpectation { @@ -152,6 +172,8 @@ final class CoreExpectation /** * Asserts that $needle is an element of the value. + * + * @return CoreExpectation */ public function toContain(mixed ...$needles): CoreExpectation { @@ -173,6 +195,8 @@ final class CoreExpectation /** * Asserts that the value starts with $expected. * + * @return CoreExpectation + * * @param non-empty-string $expected */ public function toStartWith(string $expected): CoreExpectation @@ -189,6 +213,8 @@ final class CoreExpectation /** * Asserts that the value ends with $expected. * + * @return CoreExpectation + * * @param non-empty-string $expected */ public function toEndWith(string $expected): CoreExpectation @@ -204,6 +230,8 @@ final class CoreExpectation /** * Asserts that $number matches value's Length. + * + * @return CoreExpectation */ public function toHaveLength(int $number): CoreExpectation { @@ -234,6 +262,8 @@ final class CoreExpectation /** * Asserts that $count matches the number of elements of the value. + * + * @return CoreExpectation */ public function toHaveCount(int $count): CoreExpectation { @@ -248,6 +278,8 @@ final class CoreExpectation /** * Asserts that the value contains the property $name. + * + * @return CoreExpectation */ public function toHaveProperty(string $name, mixed $value = null): CoreExpectation { @@ -267,6 +299,8 @@ final class CoreExpectation /** * Asserts that the value contains the provided properties $names. * + * @return CoreExpectation + * * @param iterable $names */ public function toHaveProperties(iterable $names): CoreExpectation @@ -280,6 +314,8 @@ final class CoreExpectation /** * Asserts that two variables have the same value. + * + * @return CoreExpectation */ public function toEqual(mixed $expected): CoreExpectation { @@ -296,6 +332,8 @@ final class CoreExpectation * are sorted before they are compared. When $expected and $this->value * are objects, each object is converted to an array containing all * private, protected and public attributes. + * + * @return CoreExpectation */ public function toEqualCanonicalizing(mixed $expected): CoreExpectation { @@ -307,6 +345,8 @@ final class CoreExpectation /** * Asserts that the absolute difference between the value and $expected * is lower than $delta. + * + * @return CoreExpectation */ public function toEqualWithDelta(mixed $expected, float $delta): CoreExpectation { @@ -319,6 +359,8 @@ final class CoreExpectation * Asserts that the value is one of the given values. * * @param iterable $values + * + * @return CoreExpectation */ public function toBeIn(iterable $values): CoreExpectation { @@ -329,6 +371,8 @@ final class CoreExpectation /** * Asserts that the value is infinite. + * + * @return CoreExpectation */ public function toBeInfinite(): CoreExpectation { @@ -341,6 +385,8 @@ final class CoreExpectation * Asserts that the value is an instance of $class. * * @param class-string $class + * + * @return CoreExpectation */ public function toBeInstanceOf(string $class): CoreExpectation { @@ -351,6 +397,8 @@ final class CoreExpectation /** * Asserts that the value is an array. + * + * @return CoreExpectation */ public function toBeArray(): CoreExpectation { @@ -361,6 +409,8 @@ final class CoreExpectation /** * Asserts that the value is of type bool. + * + * @return CoreExpectation */ public function toBeBool(): CoreExpectation { @@ -371,6 +421,8 @@ final class CoreExpectation /** * Asserts that the value is of type callable. + * + * @return CoreExpectation */ public function toBeCallable(): CoreExpectation { @@ -381,6 +433,8 @@ final class CoreExpectation /** * Asserts that the value is of type float. + * + * @return CoreExpectation */ public function toBeFloat(): CoreExpectation { @@ -391,6 +445,8 @@ final class CoreExpectation /** * Asserts that the value is of type int. + * + * @return CoreExpectation */ public function toBeInt(): CoreExpectation { @@ -401,6 +457,8 @@ final class CoreExpectation /** * Asserts that the value is of type iterable. + * + * @return CoreExpectation */ public function toBeIterable(): CoreExpectation { @@ -411,6 +469,8 @@ final class CoreExpectation /** * Asserts that the value is of type numeric. + * + * @return CoreExpectation */ public function toBeNumeric(): CoreExpectation { @@ -421,6 +481,8 @@ final class CoreExpectation /** * Asserts that the value is of type object. + * + * @return CoreExpectation */ public function toBeObject(): CoreExpectation { @@ -431,6 +493,8 @@ final class CoreExpectation /** * Asserts that the value is of type resource. + * + * @return CoreExpectation */ public function toBeResource(): CoreExpectation { @@ -441,6 +505,8 @@ final class CoreExpectation /** * Asserts that the value is of type scalar. + * + * @return CoreExpectation */ public function toBeScalar(): CoreExpectation { @@ -451,6 +517,8 @@ final class CoreExpectation /** * Asserts that the value is of type string. + * + * @return CoreExpectation */ public function toBeString(): CoreExpectation { @@ -461,6 +529,8 @@ final class CoreExpectation /** * Asserts that the value is a JSON string. + * + * @return CoreExpectation */ public function toBeJson(): CoreExpectation { @@ -474,6 +544,8 @@ final class CoreExpectation /** * Asserts that the value is NAN. + * + * @return CoreExpectation */ public function toBeNan(): CoreExpectation { @@ -484,6 +556,8 @@ final class CoreExpectation /** * Asserts that the value is null. + * + * @return CoreExpectation */ public function toBeNull(): CoreExpectation { @@ -494,6 +568,8 @@ final class CoreExpectation /** * Asserts that the value array has the provided $key. + * + * @return CoreExpectation */ public function toHaveKey(string|int $key, mixed $value = null): CoreExpectation { @@ -522,6 +598,8 @@ final class CoreExpectation * Asserts that the value array has the provided $keys. * * @param array $keys + * + * @return CoreExpectation */ public function toHaveKeys(array $keys): CoreExpectation { @@ -534,6 +612,8 @@ final class CoreExpectation /** * Asserts that the value is a directory. + * + * @return CoreExpectation */ public function toBeDirectory(): CoreExpectation { @@ -548,6 +628,8 @@ final class CoreExpectation /** * Asserts that the value is a directory and is readable. + * + * @return CoreExpectation */ public function toBeReadableDirectory(): CoreExpectation { @@ -562,6 +644,8 @@ final class CoreExpectation /** * Asserts that the value is a directory and is writable. + * + * @return CoreExpectation */ public function toBeWritableDirectory(): CoreExpectation { @@ -576,6 +660,8 @@ final class CoreExpectation /** * Asserts that the value is a file. + * + * @return CoreExpectation */ public function toBeFile(): CoreExpectation { @@ -590,6 +676,8 @@ final class CoreExpectation /** * Asserts that the value is a file and is readable. + * + * @return CoreExpectation */ public function toBeReadableFile(): CoreExpectation { @@ -604,6 +692,8 @@ final class CoreExpectation /** * Asserts that the value is a file and is writable. + * + * @return CoreExpectation */ public function toBeWritableFile(): CoreExpectation { @@ -619,6 +709,8 @@ final class CoreExpectation * Asserts that the value array matches the given array subset. * * @param iterable $array + * + * @return CoreExpectation */ public function toMatchArray(iterable|object $array): CoreExpectation { @@ -650,6 +742,8 @@ final class CoreExpectation * of the properties of an given object. * * @param iterable|object $object + * + * @return CoreExpectation */ public function toMatchObject(iterable|object $object): CoreExpectation { @@ -678,6 +772,8 @@ final class CoreExpectation /** * Asserts that the value matches a regular expression. + * + * @return CoreExpectation */ public function toMatch(string $expression): CoreExpectation { @@ -691,6 +787,8 @@ final class CoreExpectation /** * Asserts that the value matches a constraint. + * + * @return CoreExpectation */ public function toMatchConstraint(Constraint $constraint): CoreExpectation { @@ -703,6 +801,8 @@ final class CoreExpectation * Asserts that executing value throws an exception. * * @param (Closure(Throwable): mixed)|string $exception + * + * @return CoreExpectation */ public function toThrow(callable|string $exception, string $exceptionMessage = null): CoreExpectation { From 6e7890c206acf2ae79095d9f007780eaa3210033 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 29 Nov 2021 09:58:48 +0100 Subject: [PATCH 38/45] fix phpstan --- src/Expectation.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 42acbd9d..7dd13fb2 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -22,7 +22,7 @@ use PHPUnit\Framework\ExpectationFailedException; * @property Expectation $not Creates the opposite expectation. * @property Each $each Creates an expectation on each element on the traversable value. * - * @mixin CoreExpectation + * @mixin CoreExpectation */ final class Expectation { @@ -30,6 +30,7 @@ final class Expectation __call as __extendsCall; } + /** @var CoreExpectation */ private CoreExpectation $coreExpectation; /** @@ -260,6 +261,8 @@ final class Expectation * creates a new higher order expectation. * * @param array $parameters + * + * @return Expectation|HigherOrderExpectation */ public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation { @@ -311,7 +314,7 @@ final class Expectation * Dynamically calls methods on the class without any arguments * or creates a new higher order expectation. * - * @return Expectation|OppositeExpectation|Each|HigherOrderExpectation|TValue + * @return Expectation|OppositeExpectation|Each|HigherOrderExpectation|TValue */ public function __get(string $name) { @@ -320,6 +323,7 @@ final class Expectation } if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !Expectation::hasExtend($name)) { + /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } From 05f44ed84a668954c56ca2cb8eed0477a37aa7b0 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Mon, 29 Nov 2021 10:06:00 +0100 Subject: [PATCH 39/45] fix phpstan --- src/Expectation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Expectation.php b/src/Expectation.php index 7dd13fb2..86e1f121 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -262,7 +262,7 @@ final class Expectation * * @param array $parameters * - * @return Expectation|HigherOrderExpectation + * @return Expectation|HigherOrderExpectation, TValue> */ public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation { @@ -314,7 +314,7 @@ final class Expectation * Dynamically calls methods on the class without any arguments * or creates a new higher order expectation. * - * @return Expectation|OppositeExpectation|Each|HigherOrderExpectation|TValue + * @return Expectation|OppositeExpectation|Each|HigherOrderExpectation, TValue|null>|TValue */ public function __get(string $name) { From 98db677646e330543aff8deb9c601e97ec921783 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 5 Dec 2021 14:03:09 +0000 Subject: [PATCH 40/45] refacto: pipes --- ...oreExpectation.php => BaseExpectation.php} | 212 +++++++++--------- src/Concerns/Extendable.php | 65 +----- src/Concerns/Pipeable.php | 65 ++++++ .../{RetrievesValues.php => Retrievable.php} | 2 +- src/Expectation.php | 59 ++--- src/Functions.php | 9 +- src/HigherOrderExpectation.php | 4 +- src/Support/ExpectationPipeline.php | 54 +++-- src/Support/Extendable.php | 40 ---- 9 files changed, 239 insertions(+), 271 deletions(-) rename src/{CoreExpectation.php => BaseExpectation.php} (77%) create mode 100644 src/Concerns/Pipeable.php rename src/Concerns/{RetrievesValues.php => Retrievable.php} (96%) delete mode 100644 src/Support/Extendable.php diff --git a/src/CoreExpectation.php b/src/BaseExpectation.php similarity index 77% rename from src/CoreExpectation.php rename to src/BaseExpectation.php index 153a935d..b193c397 100644 --- a/src/CoreExpectation.php +++ b/src/BaseExpectation.php @@ -7,7 +7,7 @@ namespace Pest; use BadMethodCallException; use Closure; use InvalidArgumentException; -use Pest\Concerns\RetrievesValues; +use Pest\Concerns\Retrievable; use Pest\Exceptions\InvalidExpectationValue; use Pest\Support\Arr; use Pest\Support\NullClosure; @@ -26,10 +26,8 @@ use Throwable; * * @mixin Expectation */ -final class CoreExpectation +final class BaseExpectation { - use RetrievesValues; - /** * The exporter instance, if any. * @@ -53,9 +51,9 @@ final class CoreExpectation * value. Used on objects, it asserts that two * variables reference the same object. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBe(mixed $expected): CoreExpectation + public function toBe(mixed $expected): BaseExpectation { Assert::assertSame($expected, $this->value); @@ -65,9 +63,9 @@ final class CoreExpectation /** * Asserts that the value is empty. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeEmpty(): CoreExpectation + public function toBeEmpty(): BaseExpectation { Assert::assertEmpty($this->value); @@ -77,9 +75,9 @@ final class CoreExpectation /** * Asserts that the value is true. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeTrue(): CoreExpectation + public function toBeTrue(): BaseExpectation { Assert::assertTrue($this->value); @@ -89,9 +87,9 @@ final class CoreExpectation /** * Asserts that the value is truthy. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeTruthy(): CoreExpectation + public function toBeTruthy(): BaseExpectation { Assert::assertTrue((bool) $this->value); @@ -101,9 +99,9 @@ final class CoreExpectation /** * Asserts that the value is false. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeFalse(): CoreExpectation + public function toBeFalse(): BaseExpectation { Assert::assertFalse($this->value); @@ -113,9 +111,9 @@ final class CoreExpectation /** * Asserts that the value is falsy. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeFalsy(): CoreExpectation + public function toBeFalsy(): BaseExpectation { Assert::assertFalse((bool) $this->value); @@ -125,9 +123,9 @@ final class CoreExpectation /** * Asserts that the value is greater than $expected. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeGreaterThan(int|float $expected): CoreExpectation + public function toBeGreaterThan(int|float $expected): BaseExpectation { Assert::assertGreaterThan($expected, $this->value); @@ -137,9 +135,9 @@ final class CoreExpectation /** * Asserts that the value is greater than or equal to $expected. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeGreaterThanOrEqual(int|float $expected): CoreExpectation + public function toBeGreaterThanOrEqual(int|float $expected): BaseExpectation { Assert::assertGreaterThanOrEqual($expected, $this->value); @@ -149,9 +147,9 @@ final class CoreExpectation /** * Asserts that the value is less than or equal to $expected. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeLessThan(int|float $expected): CoreExpectation + public function toBeLessThan(int|float $expected): BaseExpectation { Assert::assertLessThan($expected, $this->value); @@ -161,9 +159,9 @@ final class CoreExpectation /** * Asserts that the value is less than $expected. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeLessThanOrEqual(int|float $expected): CoreExpectation + public function toBeLessThanOrEqual(int|float $expected): BaseExpectation { Assert::assertLessThanOrEqual($expected, $this->value); @@ -173,9 +171,9 @@ final class CoreExpectation /** * Asserts that $needle is an element of the value. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toContain(mixed ...$needles): CoreExpectation + public function toContain(mixed ...$needles): BaseExpectation { foreach ($needles as $needle) { if (is_string($this->value)) { @@ -195,11 +193,11 @@ final class CoreExpectation /** * Asserts that the value starts with $expected. * - * @return CoreExpectation - * * @param non-empty-string $expected + * + *@return BaseExpectation */ - public function toStartWith(string $expected): CoreExpectation + public function toStartWith(string $expected): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -213,11 +211,11 @@ final class CoreExpectation /** * Asserts that the value ends with $expected. * - * @return CoreExpectation - * * @param non-empty-string $expected + * + *@return BaseExpectation */ - public function toEndWith(string $expected): CoreExpectation + public function toEndWith(string $expected): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -231,9 +229,9 @@ final class CoreExpectation /** * Asserts that $number matches value's Length. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toHaveLength(int $number): CoreExpectation + public function toHaveLength(int $number): BaseExpectation { if (is_string($this->value)) { Assert::assertEquals($number, mb_strlen($this->value)); @@ -263,9 +261,9 @@ final class CoreExpectation /** * Asserts that $count matches the number of elements of the value. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toHaveCount(int $count): CoreExpectation + public function toHaveCount(int $count): BaseExpectation { if (!is_countable($this->value) && !is_iterable($this->value)) { InvalidExpectationValue::expected('string'); @@ -279,9 +277,9 @@ final class CoreExpectation /** * Asserts that the value contains the property $name. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toHaveProperty(string $name, mixed $value = null): CoreExpectation + public function toHaveProperty(string $name, mixed $value = null): BaseExpectation { $this->toBeObject(); @@ -299,11 +297,11 @@ final class CoreExpectation /** * Asserts that the value contains the provided properties $names. * - * @return CoreExpectation - * * @param iterable $names + * + *@return BaseExpectation */ - public function toHaveProperties(iterable $names): CoreExpectation + public function toHaveProperties(iterable $names): BaseExpectation { foreach ($names as $name) { $this->toHaveProperty($name); @@ -315,9 +313,9 @@ final class CoreExpectation /** * Asserts that two variables have the same value. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toEqual(mixed $expected): CoreExpectation + public function toEqual(mixed $expected): BaseExpectation { Assert::assertEquals($expected, $this->value); @@ -333,9 +331,9 @@ final class CoreExpectation * are objects, each object is converted to an array containing all * private, protected and public attributes. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toEqualCanonicalizing(mixed $expected): CoreExpectation + public function toEqualCanonicalizing(mixed $expected): BaseExpectation { Assert::assertEqualsCanonicalizing($expected, $this->value); @@ -346,9 +344,9 @@ final class CoreExpectation * Asserts that the absolute difference between the value and $expected * is lower than $delta. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toEqualWithDelta(mixed $expected, float $delta): CoreExpectation + public function toEqualWithDelta(mixed $expected, float $delta): BaseExpectation { Assert::assertEqualsWithDelta($expected, $this->value, $delta); @@ -360,9 +358,9 @@ final class CoreExpectation * * @param iterable $values * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeIn(iterable $values): CoreExpectation + public function toBeIn(iterable $values): BaseExpectation { Assert::assertContains($this->value, $values); @@ -372,9 +370,9 @@ final class CoreExpectation /** * Asserts that the value is infinite. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeInfinite(): CoreExpectation + public function toBeInfinite(): BaseExpectation { Assert::assertInfinite($this->value); @@ -386,9 +384,9 @@ final class CoreExpectation * * @param class-string $class * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeInstanceOf(string $class): CoreExpectation + public function toBeInstanceOf(string $class): BaseExpectation { Assert::assertInstanceOf($class, $this->value); @@ -398,9 +396,9 @@ final class CoreExpectation /** * Asserts that the value is an array. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeArray(): CoreExpectation + public function toBeArray(): BaseExpectation { Assert::assertIsArray($this->value); @@ -410,9 +408,9 @@ final class CoreExpectation /** * Asserts that the value is of type bool. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeBool(): CoreExpectation + public function toBeBool(): BaseExpectation { Assert::assertIsBool($this->value); @@ -422,9 +420,9 @@ final class CoreExpectation /** * Asserts that the value is of type callable. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeCallable(): CoreExpectation + public function toBeCallable(): BaseExpectation { Assert::assertIsCallable($this->value); @@ -434,9 +432,9 @@ final class CoreExpectation /** * Asserts that the value is of type float. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeFloat(): CoreExpectation + public function toBeFloat(): BaseExpectation { Assert::assertIsFloat($this->value); @@ -446,9 +444,9 @@ final class CoreExpectation /** * Asserts that the value is of type int. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeInt(): CoreExpectation + public function toBeInt(): BaseExpectation { Assert::assertIsInt($this->value); @@ -458,9 +456,9 @@ final class CoreExpectation /** * Asserts that the value is of type iterable. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeIterable(): CoreExpectation + public function toBeIterable(): BaseExpectation { Assert::assertIsIterable($this->value); @@ -470,9 +468,9 @@ final class CoreExpectation /** * Asserts that the value is of type numeric. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeNumeric(): CoreExpectation + public function toBeNumeric(): BaseExpectation { Assert::assertIsNumeric($this->value); @@ -482,9 +480,9 @@ final class CoreExpectation /** * Asserts that the value is of type object. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeObject(): CoreExpectation + public function toBeObject(): BaseExpectation { Assert::assertIsObject($this->value); @@ -494,9 +492,9 @@ final class CoreExpectation /** * Asserts that the value is of type resource. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeResource(): CoreExpectation + public function toBeResource(): BaseExpectation { Assert::assertIsResource($this->value); @@ -506,9 +504,9 @@ final class CoreExpectation /** * Asserts that the value is of type scalar. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeScalar(): CoreExpectation + public function toBeScalar(): BaseExpectation { Assert::assertIsScalar($this->value); @@ -518,9 +516,9 @@ final class CoreExpectation /** * Asserts that the value is of type string. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeString(): CoreExpectation + public function toBeString(): BaseExpectation { Assert::assertIsString($this->value); @@ -530,9 +528,9 @@ final class CoreExpectation /** * Asserts that the value is a JSON string. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeJson(): CoreExpectation + public function toBeJson(): BaseExpectation { Assert::assertIsString($this->value); @@ -545,9 +543,9 @@ final class CoreExpectation /** * Asserts that the value is NAN. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeNan(): CoreExpectation + public function toBeNan(): BaseExpectation { Assert::assertNan($this->value); @@ -557,9 +555,9 @@ final class CoreExpectation /** * Asserts that the value is null. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeNull(): CoreExpectation + public function toBeNull(): BaseExpectation { Assert::assertNull($this->value); @@ -569,9 +567,9 @@ final class CoreExpectation /** * Asserts that the value array has the provided $key. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toHaveKey(string|int $key, mixed $value = null): CoreExpectation + public function toHaveKey(string|int $key, mixed $value = null): BaseExpectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); @@ -599,9 +597,9 @@ final class CoreExpectation * * @param array $keys * - * @return CoreExpectation + * @return BaseExpectation */ - public function toHaveKeys(array $keys): CoreExpectation + public function toHaveKeys(array $keys): BaseExpectation { foreach ($keys as $key) { $this->toHaveKey($key); @@ -613,9 +611,9 @@ final class CoreExpectation /** * Asserts that the value is a directory. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeDirectory(): CoreExpectation + public function toBeDirectory(): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -629,9 +627,9 @@ final class CoreExpectation /** * Asserts that the value is a directory and is readable. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeReadableDirectory(): CoreExpectation + public function toBeReadableDirectory(): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -645,9 +643,9 @@ final class CoreExpectation /** * Asserts that the value is a directory and is writable. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeWritableDirectory(): CoreExpectation + public function toBeWritableDirectory(): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -661,9 +659,9 @@ final class CoreExpectation /** * Asserts that the value is a file. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeFile(): CoreExpectation + public function toBeFile(): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -677,9 +675,9 @@ final class CoreExpectation /** * Asserts that the value is a file and is readable. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeReadableFile(): CoreExpectation + public function toBeReadableFile(): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -693,9 +691,9 @@ final class CoreExpectation /** * Asserts that the value is a file and is writable. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toBeWritableFile(): CoreExpectation + public function toBeWritableFile(): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -710,9 +708,9 @@ final class CoreExpectation * * @param iterable $array * - * @return CoreExpectation + * @return BaseExpectation */ - public function toMatchArray(iterable|object $array): CoreExpectation + public function toMatchArray(iterable|object $array): BaseExpectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $valueAsArray = $this->value->toArray(); @@ -743,9 +741,9 @@ final class CoreExpectation * * @param iterable|object $object * - * @return CoreExpectation + * @return BaseExpectation */ - public function toMatchObject(iterable|object $object): CoreExpectation + public function toMatchObject(iterable|object $object): BaseExpectation { foreach ((array) $object as $property => $value) { if (!is_object($this->value) && !is_string($this->value)) { @@ -773,9 +771,9 @@ final class CoreExpectation /** * Asserts that the value matches a regular expression. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toMatch(string $expression): CoreExpectation + public function toMatch(string $expression): BaseExpectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -788,9 +786,9 @@ final class CoreExpectation /** * Asserts that the value matches a constraint. * - * @return CoreExpectation + * @return BaseExpectation */ - public function toMatchConstraint(Constraint $constraint): CoreExpectation + public function toMatchConstraint(Constraint $constraint): BaseExpectation { Assert::assertThat($this->value, $constraint); @@ -802,9 +800,9 @@ final class CoreExpectation * * @param (Closure(Throwable): mixed)|string $exception * - * @return CoreExpectation + * @return BaseExpectation */ - public function toThrow(callable|string $exception, string $exceptionMessage = null): CoreExpectation + public function toThrow(callable|string $exception, string $exceptionMessage = null): BaseExpectation { $callback = NullClosure::create(); diff --git a/src/Concerns/Extendable.php b/src/Concerns/Extendable.php index 6f83693a..a2f7e40b 100644 --- a/src/Concerns/Extendable.php +++ b/src/Concerns/Extendable.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Pest\Concerns; -use BadMethodCallException; use Closure; /** @@ -19,51 +18,14 @@ trait Extendable */ private static array $extends = []; - /** @var array> */ - private static array $pipes = []; - /** * Register a new extend. */ - public static function extend(string $name, Closure $extend): void + public function extend(string $name, Closure $extend): void { 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; - } - - /** - * Register an interceptor that should replace an existing expectation. - * - * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter - */ - 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, ...$arguments)) { - //@phpstan-ignore-next-line - $handler->bindTo($this, get_class($this))(...$arguments); - - return; - } - - $next(); - }); - } - /** * Checks if given extend name is registered. */ @@ -71,29 +33,4 @@ trait Extendable { return array_key_exists($name, static::$extends); } - - /** - * @return array - */ - private function pipes(string $name, object $context, string $scope): array - { - return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); - } - - /** - * Dynamically handle calls to the class. - * - * @param array $parameters - */ - public function __call(string $method, array $parameters): mixed - { - if (!static::hasExtend($method)) { - throw new BadMethodCallException("$method is not a callable method name."); - } - - /** @var Closure $extend */ - $extend = static::$extends[$method]->bindTo($this, static::class); - - return $extend(...$parameters); - } } diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php new file mode 100644 index 00000000..79f32055 --- /dev/null +++ b/src/Concerns/Pipeable.php @@ -0,0 +1,65 @@ +> + */ + private static array $pipes = []; + + /** + * Register a pipe to be applied before an expectation is checked. + */ + public function pipe(string $name, Closure $pipe): void + { + self::$pipes[$name][] = $pipe; + } + + /** + * Register an interceptor that should replace an existing expectation. + * + * @param string|Closure(mixed $value, mixed ...$arguments):bool $filter + */ + public function intercept(string $name, string|Closure $filter, Closure $handler): void + { + if (is_string($filter)) { + $filter = function ($value) use ($filter): bool { + return $value instanceof $filter; + }; + } + + $this->pipe($name, function ($next, ...$arguments) use ($handler, $filter) { + /* @phpstan-ignore-next-line */ + if ($filter($this->value, ...$arguments)) { + //@phpstan-ignore-next-line + $handler->bindTo($this, get_class($this))(...$arguments); + + return; + } + + $next(); + }); + } + + /** + * Get th list of pipes by the given name. + * + * @return array + */ + private function pipes(string $name, object $context, string $scope): array + { + return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []); + } +} diff --git a/src/Concerns/RetrievesValues.php b/src/Concerns/Retrievable.php similarity index 96% rename from src/Concerns/RetrievesValues.php rename to src/Concerns/Retrievable.php index c7c6cdd9..81dd75a3 100644 --- a/src/Concerns/RetrievesValues.php +++ b/src/Concerns/Retrievable.php @@ -7,7 +7,7 @@ namespace Pest\Concerns; /** * @internal */ -trait RetrievesValues +trait Retrievable { /** * @template TRetrievableValue diff --git a/src/Expectation.php b/src/Expectation.php index 86e1f121..818a1606 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -7,7 +7,8 @@ namespace Pest; use BadMethodCallException; use Closure; use Pest\Concerns\Extendable; -use Pest\Concerns\RetrievesValues; +use Pest\Concerns\Pipeable; +use Pest\Concerns\Retrievable; use Pest\Exceptions\InvalidExpectationValue; use Pest\Exceptions\PipeException; use Pest\Support\ExpectationPipeline; @@ -22,25 +23,23 @@ use PHPUnit\Framework\ExpectationFailedException; * @property Expectation $not Creates the opposite expectation. * @property Each $each Creates an expectation on each element on the traversable value. * - * @mixin CoreExpectation + * @mixin BaseExpectation */ final class Expectation { - use RetrievesValues, Extendable { - __call as __extendsCall; - } - - /** @var CoreExpectation */ - private CoreExpectation $coreExpectation; + use Retrievable; + use Pipeable; + use Extendable; /** * Creates a new expectation. * * @param TValue $value */ - public function __construct(mixed $value) - { - $this->coreExpectation = new CoreExpectation($value); + public function __construct( + public mixed $value + ) { + // .. } /** @@ -257,8 +256,7 @@ final class Expectation } /** - * Dynamically handle calls to the class or - * creates a new higher order expectation. + * Dynamically calls methods on the class or creates a new higher order expectation. * * @param array $parameters * @@ -266,7 +264,7 @@ final class Expectation */ public function __call(string $method, array $parameters): Expectation|HigherOrderExpectation { - if (!$this->hasExpectation($method)) { + if (!self::hasMethod($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } @@ -281,9 +279,9 @@ final class Expectation private function getExpectationClosure(string $name): Closure { - if (method_exists($this->coreExpectation, $name)) { + if (method_exists(BaseExpectation::class, $name)) { //@phpstan-ignore-next-line - return Closure::fromCallable([$this->coreExpectation, $name]); + return Closure::fromCallable([new BaseExpectation($this->value), $name]); } if (self::hasExtend($name)) { @@ -297,32 +295,14 @@ final class Expectation throw PipeException::expectationNotFound($name); } - private function hasExpectation(string $name): bool - { - if (method_exists($this->coreExpectation, $name)) { - return true; - } - - if (self::hasExtend($name)) { - return true; - } - - return false; - } - /** - * Dynamically calls methods on the class without any arguments - * or creates a new higher order expectation. + * Dynamically calls methods on the class without any arguments or creates a new higher order expectation. * * @return Expectation|OppositeExpectation|Each|HigherOrderExpectation, TValue|null>|TValue */ public function __get(string $name) { - if ($name === 'value') { - return $this->coreExpectation->value; - } - - if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !Expectation::hasExtend($name)) { + if (!self::hasMethod($name)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } @@ -331,8 +311,13 @@ final class Expectation return $this->{$name}(); } + /** + * Checks if the given expectation method exists. + */ public static function hasMethod(string $name): bool { - return method_exists(CoreExpectation::class, $name); + return method_exists(self::class, $name) + || method_exists(BaseExpectation::class, $name) + || self::hasExtend($name); } } diff --git a/src/Functions.php b/src/Functions.php index 80e0d9e0..00eed63d 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -9,7 +9,6 @@ use Pest\PendingCalls\BeforeEachCall; use Pest\PendingCalls\TestCall; use Pest\PendingCalls\UsesCall; use Pest\Support\Backtrace; -use Pest\Support\Extendable; use Pest\Support\HigherOrderTapProxy; use Pest\TestSuite; use PHPUnit\Framework\TestCase; @@ -22,14 +21,10 @@ if (!function_exists('expect')) { * * @param TValue $value the Value * - * @return Expectation|Extendable + * @return Expectation */ - function expect($value = null): Expectation|Extendable + function expect($value = null): Expectation { - if (func_num_args() === 0) { - return new Extendable(Expectation::class); - } - return new Expectation($value); } } diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index 8c4dcc4e..6480f0d7 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Pest; -use Pest\Concerns\RetrievesValues; +use Pest\Concerns\Retrievable; /** * @internal @@ -16,7 +16,7 @@ use Pest\Concerns\RetrievesValues; */ final class HigherOrderExpectation { - use RetrievesValues; + use Retrievable; /** * @var Expectation|Each diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php index 0ddc9260..11481576 100644 --- a/src/Support/ExpectationPipeline.php +++ b/src/Support/ExpectationPipeline.php @@ -11,33 +11,55 @@ use Closure; */ final class ExpectationPipeline { - /** @var array */ + /** + * The list of pipes. + * + * @var array + */ private array $pipes = []; - /** @var array */ - private array $passable; + /** + * The list of passables. + * + * @var array + */ + private array $passables; - private Closure $expectationClosure; + /** + * The expectation closure. + */ + private Closure $closure; - public function __construct(Closure $expectationClosure) + /** + * Creates a new instance of Expectation Pipeline. + */ + public function __construct(Closure $closure) { - $this->expectationClosure = $expectationClosure; + $this->closure = $closure; } - public static function for(Closure $expectationClosure): self + /** + * Creates a new instance of Expectation Pipeline with given closure. + */ + public static function for(Closure $closure): self { - return new self($expectationClosure); + return new self($closure); } - public function send(mixed ...$passable): self + /** + * Sets the list of passables. + */ + public function send(mixed ...$passables): self { - $this->passable = $passable; + $this->passables = array_values($passables); return $this; } /** - * @param array $pipes + * Sets the list of pipes. + * + * @param array $pipes */ public function through(array $pipes): self { @@ -46,24 +68,30 @@ final class ExpectationPipeline return $this; } + /** + * Runs the pipeline. + */ public function run(): void { $pipeline = array_reduce( array_reverse($this->pipes), $this->carry(), function (): void { - ($this->expectationClosure)(...$this->passable); + ($this->closure)(...$this->passables); } ); $pipeline(); } + /** + * Get a Closure that will carry of the expectation. + */ public function carry(): Closure { return function ($stack, $pipe): Closure { return function () use ($stack, $pipe) { - return $pipe($stack, ...$this->passable); + return $pipe($stack, ...$this->passables); }; }; } diff --git a/src/Support/Extendable.php b/src/Support/Extendable.php deleted file mode 100644 index fba8e17b..00000000 --- a/src/Support/Extendable.php +++ /dev/null @@ -1,40 +0,0 @@ -extendableClass::extend($name, $extend); - } - - public function pipe(string $name, Closure $pipe): void - { - $this->extendableClass::pipe($name, $pipe); - } - - /** - * @param string|Closure $filter - */ - public function intercept(string $name, $filter, Closure $handler): void - { - $this->extendableClass::intercept($name, $filter, $handler); - } -} From c8697e031012e8307283e02e6df1f6d773327118 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 5 Dec 2021 14:05:01 +0000 Subject: [PATCH 41/45] chore: fixes style --- src/BaseExpectation.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BaseExpectation.php b/src/BaseExpectation.php index b193c397..4787b8fe 100644 --- a/src/BaseExpectation.php +++ b/src/BaseExpectation.php @@ -7,7 +7,6 @@ namespace Pest; use BadMethodCallException; use Closure; use InvalidArgumentException; -use Pest\Concerns\Retrievable; use Pest\Exceptions\InvalidExpectationValue; use Pest\Support\Arr; use Pest\Support\NullClosure; From e64b6fe924df553006db2cdeeee7713f5859f7ac Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 5 Dec 2021 14:21:11 +0000 Subject: [PATCH 42/45] refacto: pipes --- src/Expectation.php | 9 +- src/Functions.php | 4 +- .../Expectation.php} | 206 +++++++++--------- 3 files changed, 110 insertions(+), 109 deletions(-) rename src/{BaseExpectation.php => Mixins/Expectation.php} (77%) diff --git a/src/Expectation.php b/src/Expectation.php index 818a1606..c0e0e5ec 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Pest; use BadMethodCallException; +use Carbon\Traits\Mixin; use Closure; use Pest\Concerns\Extendable; use Pest\Concerns\Pipeable; @@ -23,7 +24,7 @@ use PHPUnit\Framework\ExpectationFailedException; * @property Expectation $not Creates the opposite expectation. * @property Each $each Creates an expectation on each element on the traversable value. * - * @mixin BaseExpectation + * @mixin Mixins\Expectation */ final class Expectation { @@ -279,9 +280,9 @@ final class Expectation private function getExpectationClosure(string $name): Closure { - if (method_exists(BaseExpectation::class, $name)) { + if (method_exists(Mixins\Expectation::class, $name)) { //@phpstan-ignore-next-line - return Closure::fromCallable([new BaseExpectation($this->value), $name]); + return Closure::fromCallable([new Mixins\Expectation($this->value), $name]); } if (self::hasExtend($name)) { @@ -317,7 +318,7 @@ final class Expectation public static function hasMethod(string $name): bool { return method_exists(self::class, $name) - || method_exists(BaseExpectation::class, $name) + || method_exists(Mixins\Expectation::class, $name) || self::hasExtend($name); } } diff --git a/src/Functions.php b/src/Functions.php index 00eed63d..4d07a05f 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -19,11 +19,11 @@ if (!function_exists('expect')) { * * @template TValue * - * @param TValue $value the Value + * @param TValue $value * * @return Expectation */ - function expect($value = null): Expectation + function expect(mixed $value = null): Expectation { return new Expectation($value); } diff --git a/src/BaseExpectation.php b/src/Mixins/Expectation.php similarity index 77% rename from src/BaseExpectation.php rename to src/Mixins/Expectation.php index 4787b8fe..3fbd2110 100644 --- a/src/BaseExpectation.php +++ b/src/Mixins/Expectation.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Pest; +namespace Pest\Mixins; use BadMethodCallException; use Closure; @@ -23,9 +23,9 @@ use Throwable; * * @template TValue * - * @mixin Expectation + * @mixin \Pest\Expectation */ -final class BaseExpectation +final class Expectation { /** * The exporter instance, if any. @@ -50,9 +50,9 @@ final class BaseExpectation * value. Used on objects, it asserts that two * variables reference the same object. * - * @return BaseExpectation + * @return Expectation */ - public function toBe(mixed $expected): BaseExpectation + public function toBe(mixed $expected): Expectation { Assert::assertSame($expected, $this->value); @@ -62,9 +62,9 @@ final class BaseExpectation /** * Asserts that the value is empty. * - * @return BaseExpectation + * @return Expectation */ - public function toBeEmpty(): BaseExpectation + public function toBeEmpty(): Expectation { Assert::assertEmpty($this->value); @@ -74,9 +74,9 @@ final class BaseExpectation /** * Asserts that the value is true. * - * @return BaseExpectation + * @return Expectation */ - public function toBeTrue(): BaseExpectation + public function toBeTrue(): Expectation { Assert::assertTrue($this->value); @@ -86,9 +86,9 @@ final class BaseExpectation /** * Asserts that the value is truthy. * - * @return BaseExpectation + * @return Expectation */ - public function toBeTruthy(): BaseExpectation + public function toBeTruthy(): Expectation { Assert::assertTrue((bool) $this->value); @@ -98,9 +98,9 @@ final class BaseExpectation /** * Asserts that the value is false. * - * @return BaseExpectation + * @return Expectation */ - public function toBeFalse(): BaseExpectation + public function toBeFalse(): Expectation { Assert::assertFalse($this->value); @@ -110,9 +110,9 @@ final class BaseExpectation /** * Asserts that the value is falsy. * - * @return BaseExpectation + * @return Expectation */ - public function toBeFalsy(): BaseExpectation + public function toBeFalsy(): Expectation { Assert::assertFalse((bool) $this->value); @@ -122,9 +122,9 @@ final class BaseExpectation /** * Asserts that the value is greater than $expected. * - * @return BaseExpectation + * @return Expectation */ - public function toBeGreaterThan(int|float $expected): BaseExpectation + public function toBeGreaterThan(int|float $expected): Expectation { Assert::assertGreaterThan($expected, $this->value); @@ -134,9 +134,9 @@ final class BaseExpectation /** * Asserts that the value is greater than or equal to $expected. * - * @return BaseExpectation + * @return Expectation */ - public function toBeGreaterThanOrEqual(int|float $expected): BaseExpectation + public function toBeGreaterThanOrEqual(int|float $expected): Expectation { Assert::assertGreaterThanOrEqual($expected, $this->value); @@ -146,9 +146,9 @@ final class BaseExpectation /** * Asserts that the value is less than or equal to $expected. * - * @return BaseExpectation + * @return Expectation */ - public function toBeLessThan(int|float $expected): BaseExpectation + public function toBeLessThan(int|float $expected): Expectation { Assert::assertLessThan($expected, $this->value); @@ -158,9 +158,9 @@ final class BaseExpectation /** * Asserts that the value is less than $expected. * - * @return BaseExpectation + * @return Expectation */ - public function toBeLessThanOrEqual(int|float $expected): BaseExpectation + public function toBeLessThanOrEqual(int|float $expected): Expectation { Assert::assertLessThanOrEqual($expected, $this->value); @@ -170,9 +170,9 @@ final class BaseExpectation /** * Asserts that $needle is an element of the value. * - * @return BaseExpectation + * @return Expectation */ - public function toContain(mixed ...$needles): BaseExpectation + public function toContain(mixed ...$needles): Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { @@ -194,9 +194,9 @@ final class BaseExpectation * * @param non-empty-string $expected * - *@return BaseExpectation + *@return Expectation */ - public function toStartWith(string $expected): BaseExpectation + public function toStartWith(string $expected): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -212,9 +212,9 @@ final class BaseExpectation * * @param non-empty-string $expected * - *@return BaseExpectation + *@return Expectation */ - public function toEndWith(string $expected): BaseExpectation + public function toEndWith(string $expected): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -228,9 +228,9 @@ final class BaseExpectation /** * Asserts that $number matches value's Length. * - * @return BaseExpectation + * @return Expectation */ - public function toHaveLength(int $number): BaseExpectation + public function toHaveLength(int $number): Expectation { if (is_string($this->value)) { Assert::assertEquals($number, mb_strlen($this->value)); @@ -260,9 +260,9 @@ final class BaseExpectation /** * Asserts that $count matches the number of elements of the value. * - * @return BaseExpectation + * @return Expectation */ - public function toHaveCount(int $count): BaseExpectation + public function toHaveCount(int $count): Expectation { if (!is_countable($this->value) && !is_iterable($this->value)) { InvalidExpectationValue::expected('string'); @@ -276,9 +276,9 @@ final class BaseExpectation /** * Asserts that the value contains the property $name. * - * @return BaseExpectation + * @return Expectation */ - public function toHaveProperty(string $name, mixed $value = null): BaseExpectation + public function toHaveProperty(string $name, mixed $value = null): Expectation { $this->toBeObject(); @@ -298,9 +298,9 @@ final class BaseExpectation * * @param iterable $names * - *@return BaseExpectation + *@return Expectation */ - public function toHaveProperties(iterable $names): BaseExpectation + public function toHaveProperties(iterable $names): Expectation { foreach ($names as $name) { $this->toHaveProperty($name); @@ -312,9 +312,9 @@ final class BaseExpectation /** * Asserts that two variables have the same value. * - * @return BaseExpectation + * @return Expectation */ - public function toEqual(mixed $expected): BaseExpectation + public function toEqual(mixed $expected): Expectation { Assert::assertEquals($expected, $this->value); @@ -330,9 +330,9 @@ final class BaseExpectation * are objects, each object is converted to an array containing all * private, protected and public attributes. * - * @return BaseExpectation + * @return Expectation */ - public function toEqualCanonicalizing(mixed $expected): BaseExpectation + public function toEqualCanonicalizing(mixed $expected): Expectation { Assert::assertEqualsCanonicalizing($expected, $this->value); @@ -343,9 +343,9 @@ final class BaseExpectation * Asserts that the absolute difference between the value and $expected * is lower than $delta. * - * @return BaseExpectation + * @return Expectation */ - public function toEqualWithDelta(mixed $expected, float $delta): BaseExpectation + public function toEqualWithDelta(mixed $expected, float $delta): Expectation { Assert::assertEqualsWithDelta($expected, $this->value, $delta); @@ -357,9 +357,9 @@ final class BaseExpectation * * @param iterable $values * - * @return BaseExpectation + * @return Expectation */ - public function toBeIn(iterable $values): BaseExpectation + public function toBeIn(iterable $values): Expectation { Assert::assertContains($this->value, $values); @@ -369,9 +369,9 @@ final class BaseExpectation /** * Asserts that the value is infinite. * - * @return BaseExpectation + * @return Expectation */ - public function toBeInfinite(): BaseExpectation + public function toBeInfinite(): Expectation { Assert::assertInfinite($this->value); @@ -383,9 +383,9 @@ final class BaseExpectation * * @param class-string $class * - * @return BaseExpectation + * @return Expectation */ - public function toBeInstanceOf(string $class): BaseExpectation + public function toBeInstanceOf(string $class): Expectation { Assert::assertInstanceOf($class, $this->value); @@ -395,9 +395,9 @@ final class BaseExpectation /** * Asserts that the value is an array. * - * @return BaseExpectation + * @return Expectation */ - public function toBeArray(): BaseExpectation + public function toBeArray(): Expectation { Assert::assertIsArray($this->value); @@ -407,9 +407,9 @@ final class BaseExpectation /** * Asserts that the value is of type bool. * - * @return BaseExpectation + * @return Expectation */ - public function toBeBool(): BaseExpectation + public function toBeBool(): Expectation { Assert::assertIsBool($this->value); @@ -419,9 +419,9 @@ final class BaseExpectation /** * Asserts that the value is of type callable. * - * @return BaseExpectation + * @return Expectation */ - public function toBeCallable(): BaseExpectation + public function toBeCallable(): Expectation { Assert::assertIsCallable($this->value); @@ -431,9 +431,9 @@ final class BaseExpectation /** * Asserts that the value is of type float. * - * @return BaseExpectation + * @return Expectation */ - public function toBeFloat(): BaseExpectation + public function toBeFloat(): Expectation { Assert::assertIsFloat($this->value); @@ -443,9 +443,9 @@ final class BaseExpectation /** * Asserts that the value is of type int. * - * @return BaseExpectation + * @return Expectation */ - public function toBeInt(): BaseExpectation + public function toBeInt(): Expectation { Assert::assertIsInt($this->value); @@ -455,9 +455,9 @@ final class BaseExpectation /** * Asserts that the value is of type iterable. * - * @return BaseExpectation + * @return Expectation */ - public function toBeIterable(): BaseExpectation + public function toBeIterable(): Expectation { Assert::assertIsIterable($this->value); @@ -467,9 +467,9 @@ final class BaseExpectation /** * Asserts that the value is of type numeric. * - * @return BaseExpectation + * @return Expectation */ - public function toBeNumeric(): BaseExpectation + public function toBeNumeric(): Expectation { Assert::assertIsNumeric($this->value); @@ -479,9 +479,9 @@ final class BaseExpectation /** * Asserts that the value is of type object. * - * @return BaseExpectation + * @return Expectation */ - public function toBeObject(): BaseExpectation + public function toBeObject(): Expectation { Assert::assertIsObject($this->value); @@ -491,9 +491,9 @@ final class BaseExpectation /** * Asserts that the value is of type resource. * - * @return BaseExpectation + * @return Expectation */ - public function toBeResource(): BaseExpectation + public function toBeResource(): Expectation { Assert::assertIsResource($this->value); @@ -503,9 +503,9 @@ final class BaseExpectation /** * Asserts that the value is of type scalar. * - * @return BaseExpectation + * @return Expectation */ - public function toBeScalar(): BaseExpectation + public function toBeScalar(): Expectation { Assert::assertIsScalar($this->value); @@ -515,9 +515,9 @@ final class BaseExpectation /** * Asserts that the value is of type string. * - * @return BaseExpectation + * @return Expectation */ - public function toBeString(): BaseExpectation + public function toBeString(): Expectation { Assert::assertIsString($this->value); @@ -527,9 +527,9 @@ final class BaseExpectation /** * Asserts that the value is a JSON string. * - * @return BaseExpectation + * @return Expectation */ - public function toBeJson(): BaseExpectation + public function toBeJson(): Expectation { Assert::assertIsString($this->value); @@ -542,9 +542,9 @@ final class BaseExpectation /** * Asserts that the value is NAN. * - * @return BaseExpectation + * @return Expectation */ - public function toBeNan(): BaseExpectation + public function toBeNan(): Expectation { Assert::assertNan($this->value); @@ -554,9 +554,9 @@ final class BaseExpectation /** * Asserts that the value is null. * - * @return BaseExpectation + * @return Expectation */ - public function toBeNull(): BaseExpectation + public function toBeNull(): Expectation { Assert::assertNull($this->value); @@ -566,9 +566,9 @@ final class BaseExpectation /** * Asserts that the value array has the provided $key. * - * @return BaseExpectation + * @return Expectation */ - public function toHaveKey(string|int $key, mixed $value = null): BaseExpectation + public function toHaveKey(string|int $key, mixed $value = null): Expectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $array = $this->value->toArray(); @@ -596,9 +596,9 @@ final class BaseExpectation * * @param array $keys * - * @return BaseExpectation + * @return Expectation */ - public function toHaveKeys(array $keys): BaseExpectation + public function toHaveKeys(array $keys): Expectation { foreach ($keys as $key) { $this->toHaveKey($key); @@ -610,9 +610,9 @@ final class BaseExpectation /** * Asserts that the value is a directory. * - * @return BaseExpectation + * @return Expectation */ - public function toBeDirectory(): BaseExpectation + public function toBeDirectory(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -626,9 +626,9 @@ final class BaseExpectation /** * Asserts that the value is a directory and is readable. * - * @return BaseExpectation + * @return Expectation */ - public function toBeReadableDirectory(): BaseExpectation + public function toBeReadableDirectory(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -642,9 +642,9 @@ final class BaseExpectation /** * Asserts that the value is a directory and is writable. * - * @return BaseExpectation + * @return Expectation */ - public function toBeWritableDirectory(): BaseExpectation + public function toBeWritableDirectory(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -658,9 +658,9 @@ final class BaseExpectation /** * Asserts that the value is a file. * - * @return BaseExpectation + * @return Expectation */ - public function toBeFile(): BaseExpectation + public function toBeFile(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -674,9 +674,9 @@ final class BaseExpectation /** * Asserts that the value is a file and is readable. * - * @return BaseExpectation + * @return Expectation */ - public function toBeReadableFile(): BaseExpectation + public function toBeReadableFile(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -690,9 +690,9 @@ final class BaseExpectation /** * Asserts that the value is a file and is writable. * - * @return BaseExpectation + * @return Expectation */ - public function toBeWritableFile(): BaseExpectation + public function toBeWritableFile(): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -707,9 +707,9 @@ final class BaseExpectation * * @param iterable $array * - * @return BaseExpectation + * @return Expectation */ - public function toMatchArray(iterable|object $array): BaseExpectation + public function toMatchArray(iterable|object $array): Expectation { if (is_object($this->value) && method_exists($this->value, 'toArray')) { $valueAsArray = $this->value->toArray(); @@ -740,9 +740,9 @@ final class BaseExpectation * * @param iterable|object $object * - * @return BaseExpectation + * @return Expectation */ - public function toMatchObject(iterable|object $object): BaseExpectation + public function toMatchObject(iterable|object $object): Expectation { foreach ((array) $object as $property => $value) { if (!is_object($this->value) && !is_string($this->value)) { @@ -770,9 +770,9 @@ final class BaseExpectation /** * Asserts that the value matches a regular expression. * - * @return BaseExpectation + * @return Expectation */ - public function toMatch(string $expression): BaseExpectation + public function toMatch(string $expression): Expectation { if (!is_string($this->value)) { InvalidExpectationValue::expected('string'); @@ -785,9 +785,9 @@ final class BaseExpectation /** * Asserts that the value matches a constraint. * - * @return BaseExpectation + * @return Expectation */ - public function toMatchConstraint(Constraint $constraint): BaseExpectation + public function toMatchConstraint(Constraint $constraint): Expectation { Assert::assertThat($this->value, $constraint); @@ -799,9 +799,9 @@ final class BaseExpectation * * @param (Closure(Throwable): mixed)|string $exception * - * @return BaseExpectation + * @return Expectation */ - public function toThrow(callable|string $exception, string $exceptionMessage = null): BaseExpectation + public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation { $callback = NullClosure::create(); From b1f9ce228312a072835d6fa8f8b64e5a0cbc49fd Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 5 Dec 2021 14:40:08 +0000 Subject: [PATCH 43/45] refacto: structure --- overrides/Runner/TestSuiteLoader.php | 2 +- src/Exceptions/ExpectationNotFound.php | 21 ++++++++++++++ src/Exceptions/PipeException.php | 15 ---------- src/Expectation.php | 29 ++++++++++++------- .../EachExpectation.php} | 13 +++++---- .../HigherOrderExpectation.php | 7 +++-- .../OppositeExpectation.php | 3 +- src/Factories/TestCaseFactory.php | 2 +- src/Factories/TestCaseMethodFactory.php | 4 +-- src/Functions.php | 4 +-- src/Laravel/Commands/PestDatasetCommand.php | 2 +- src/Plugin.php | 3 +- .../DatasetsRepository.php} | 4 +-- src/{ => TestCases}/IgnorableTestCase.php | 2 +- tests/Features/Datasets.php | 16 +++++----- tests/Features/Expect/pipes.php | 1 + tests/Unit/Datasets.php | 12 ++++---- 17 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 src/Exceptions/ExpectationNotFound.php delete mode 100644 src/Exceptions/PipeException.php rename src/{Each.php => Expectations/EachExpectation.php} (88%) rename src/{ => Expectations}/HigherOrderExpectation.php (95%) rename src/{ => Expectations}/OppositeExpectation.php (98%) rename src/{Datasets.php => Repositories/DatasetsRepository.php} (98%) rename src/{ => TestCases}/IgnorableTestCase.php (85%) diff --git a/overrides/Runner/TestSuiteLoader.php b/overrides/Runner/TestSuiteLoader.php index 19b37a82..5e59ae21 100644 --- a/overrides/Runner/TestSuiteLoader.php +++ b/overrides/Runner/TestSuiteLoader.php @@ -43,7 +43,7 @@ use function array_values; use function basename; use function class_exists; use function get_declared_classes; -use Pest\IgnorableTestCase; +use Pest\TestCases\IgnorableTestCase; use Pest\TestSuite; use PHPUnit\Framework\TestCase; use ReflectionClass; diff --git a/src/Exceptions/ExpectationNotFound.php b/src/Exceptions/ExpectationNotFound.php new file mode 100644 index 00000000..af47c03f --- /dev/null +++ b/src/Exceptions/ExpectationNotFound.php @@ -0,0 +1,21 @@ + */ final class Expectation { - use Retrievable; - use Pipeable; use Extendable; + use Pipeable; + use Retrievable; /** * Creates a new expectation. @@ -114,9 +116,9 @@ final class Expectation /** * Creates an expectation on each item of the iterable "value". * - * @return Each + * @return EachExpectation */ - public function each(callable $callback = null): Each + public function each(callable $callback = null): EachExpectation { if (!is_iterable($this->value)) { throw new BadMethodCallException('Expectation value is not iterable.'); @@ -128,7 +130,7 @@ final class Expectation } } - return new Each($this); + return new EachExpectation($this); } /** @@ -278,6 +280,11 @@ final class Expectation return $this; } + /** + * Creates a new expectation closure from the given name. + * + * @throws ExpectationNotFound + */ private function getExpectationClosure(string $name): Closure { if (method_exists(Mixins\Expectation::class, $name)) { @@ -293,13 +300,13 @@ final class Expectation } } - throw PipeException::expectationNotFound($name); + throw ExpectationNotFound::fromName($name); } /** * Dynamically calls methods on the class without any arguments or creates a new higher order expectation. * - * @return Expectation|OppositeExpectation|Each|HigherOrderExpectation, TValue|null>|TValue + * @return Expectation|OppositeExpectation|EachExpectation|HigherOrderExpectation, TValue|null>|TValue */ public function __get(string $name) { diff --git a/src/Each.php b/src/Expectations/EachExpectation.php similarity index 88% rename from src/Each.php rename to src/Expectations/EachExpectation.php index 19105945..df0cb1d0 100644 --- a/src/Each.php +++ b/src/Expectations/EachExpectation.php @@ -2,7 +2,10 @@ declare(strict_types=1); -namespace Pest; +namespace Pest\Expectations; + +use function expect; +use Pest\Expectation; /** * @internal @@ -11,7 +14,7 @@ namespace Pest; * * @mixin Expectation */ -final class Each +final class EachExpectation { private bool $opposite = false; @@ -43,7 +46,7 @@ final class Each * * @return self */ - public function not(): Each + public function not(): EachExpectation { $this->opposite = true; @@ -57,7 +60,7 @@ final class Each * * @return self */ - public function __call(string $name, array $arguments): Each + public function __call(string $name, array $arguments): EachExpectation { foreach ($this->original->value as $item) { /* @phpstan-ignore-next-line */ @@ -74,7 +77,7 @@ final class Each * * @return self */ - public function __get(string $name): Each + public function __get(string $name): EachExpectation { /* @phpstan-ignore-next-line */ return $this->$name(); diff --git a/src/HigherOrderExpectation.php b/src/Expectations/HigherOrderExpectation.php similarity index 95% rename from src/HigherOrderExpectation.php rename to src/Expectations/HigherOrderExpectation.php index 6480f0d7..83c43643 100644 --- a/src/HigherOrderExpectation.php +++ b/src/Expectations/HigherOrderExpectation.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace Pest; +namespace Pest\Expectations; use Pest\Concerns\Retrievable; +use Pest\Expectation; /** * @internal @@ -19,9 +20,9 @@ final class HigherOrderExpectation use Retrievable; /** - * @var Expectation|Each + * @var Expectation|EachExpectation */ - private Expectation|Each $expectation; + private Expectation|EachExpectation $expectation; private bool $opposite = false; diff --git a/src/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php similarity index 98% rename from src/OppositeExpectation.php rename to src/Expectations/OppositeExpectation.php index e2062e75..1ae9ccbe 100644 --- a/src/OppositeExpectation.php +++ b/src/Expectations/OppositeExpectation.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Pest; +namespace Pest\Expectations; +use Pest\Expectation; use PHPUnit\Framework\ExpectationFailedException; use SebastianBergmann\Exporter\Exporter; diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index b214631b..56686443 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -150,7 +150,7 @@ final class TestCaseFactory eval(" namespace $namespace; - use Pest\Datasets as __PestDatasets; + use Pest\Repositories\DatasetsRepository as __PestDatasets; use Pest\TestSuite as __PestTestSuite; final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 1aaa2dae..d895db73 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -5,9 +5,9 @@ declare(strict_types=1); namespace Pest\Factories; use Closure; -use Pest\Datasets; use Pest\Exceptions\ShouldNotHappen; use Pest\Factories\Concerns\HigherOrderable; +use Pest\Repositories\DatasetsRepository; use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\Assert; @@ -154,7 +154,7 @@ final class TestCaseMethodFactory */ private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string { - Datasets::with($this->filename, $methodName, $this->datasets); + DatasetsRepository::with($this->filename, $methodName, $this->datasets); return <<argument('name'); - $relativePath = sprintf(testDirectory('Datasets/%s.php'), ucfirst($name)); + $relativePath = sprintf(testDirectory('DatasetsRepository/%s.php'), ucfirst($name)); /* @phpstan-ignore-next-line */ $target = base_path($relativePath); diff --git a/src/Plugin.php b/src/Plugin.php index 5f676be4..6e73d59c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -7,8 +7,7 @@ namespace Pest; final class Plugin { /** - * The lazy callables to be executed - * once the test suite boots. + * The lazy callables to be executed once the test suite boots. * * @var array * diff --git a/src/Datasets.php b/src/Repositories/DatasetsRepository.php similarity index 98% rename from src/Datasets.php rename to src/Repositories/DatasetsRepository.php index 248b1d38..d8a57bfa 100644 --- a/src/Datasets.php +++ b/src/Repositories/DatasetsRepository.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Pest; +namespace Pest\Repositories; use Closure; use Pest\Exceptions\DatasetAlreadyExist; @@ -15,7 +15,7 @@ use Traversable; /** * @internal */ -final class Datasets +final class DatasetsRepository { /** * Holds the datasets. diff --git a/src/IgnorableTestCase.php b/src/TestCases/IgnorableTestCase.php similarity index 85% rename from src/IgnorableTestCase.php rename to src/TestCases/IgnorableTestCase.php index ba4a4bf1..a2e9f5e9 100644 --- a/src/IgnorableTestCase.php +++ b/src/TestCases/IgnorableTestCase.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Pest; +namespace Pest\TestCases; use PHPUnit\Framework\TestCase; diff --git a/tests/Features/Datasets.php b/tests/Features/Datasets.php index c7e695e1..4b7f9dc3 100644 --- a/tests/Features/Datasets.php +++ b/tests/Features/Datasets.php @@ -1,9 +1,9 @@ foo = 'bar'; @@ -13,28 +13,28 @@ 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::resolve('foo', ['first']); + DatasetsRepository::resolve('foo', ['first']); }); it('throws exception if dataset already exist', function () { - Datasets::set('second', [[]]); + DatasetsRepository::set('second', [[]]); $this->expectException(DatasetAlreadyExist::class); $this->expectExceptionMessage('A dataset with the name `second` already exist.'); - Datasets::set('second', [[]]); + DatasetsRepository::set('second', [[]]); }); it('sets closures', function () { - Datasets::set('foo', function () { + DatasetsRepository::set('foo', function () { yield [1]; }); - expect(Datasets::resolve('foo', ['foo']))->toBe(['foo with (1)' => [1]]); + expect(DatasetsRepository::resolve('foo', ['foo']))->toBe(['foo with (1)' => [1]]); }); it('sets arrays', function () { - Datasets::set('bar', [[2]]); + DatasetsRepository::set('bar', [[2]]); - expect(Datasets::resolve('bar', ['bar']))->toBe(['bar with (2)' => [2]]); + expect(DatasetsRepository::resolve('bar', ['bar']))->toBe(['bar with (2)' => [2]]); }); it('gets bound to test case object', function () { diff --git a/tests/Features/Expect/pipes.php b/tests/Features/Expect/pipes.php index 5e311e07..e6808dbb 100644 --- a/tests/Features/Expect/pipes.php +++ b/tests/Features/Expect/pipes.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Pest\Exceptions\ExpectationNotFound; use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertEqualsIgnoringCase; use function PHPUnit\Framework\assertInstanceOf; diff --git a/tests/Unit/Datasets.php b/tests/Unit/Datasets.php index 08f82352..a93ac62e 100644 --- a/tests/Unit/Datasets.php +++ b/tests/Unit/Datasets.php @@ -1,9 +1,9 @@ [1], 'two' => [[2]], @@ -15,7 +15,7 @@ it('show only the names of named datasets in their description', function () { }); it('show the actual dataset of non-named datasets in their description', function () { - $descriptions = array_keys(Datasets::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve('test description', [ [ [1], [[2]], @@ -27,7 +27,7 @@ it('show the actual dataset of non-named datasets in their description', functio }); it('show only the names of multiple named datasets in their description', function () { - $descriptions = array_keys(Datasets::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve('test description', [ [ 'one' => [1], 'two' => [[2]], @@ -45,7 +45,7 @@ it('show only the names of multiple named datasets in their description', functi }); it('show the actual dataset of multiple non-named datasets in their description', function () { - $descriptions = array_keys(Datasets::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve('test description', [ [ [1], [[2]], @@ -63,7 +63,7 @@ it('show the actual dataset of multiple non-named datasets in their description' }); it('show the correct description for mixed named and not-named datasets', function () { - $descriptions = array_keys(Datasets::resolve('test description', [ + $descriptions = array_keys(DatasetsRepository::resolve('test description', [ [ 'one' => [1], [[2]], From b74a688677b811a0adbd7a75f9abb46b8c5b0001 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 5 Dec 2021 14:59:07 +0000 Subject: [PATCH 44/45] tests: style --- tests/Features/Expect/pipes.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Features/Expect/pipes.php b/tests/Features/Expect/pipes.php index e6808dbb..5e311e07 100644 --- a/tests/Features/Expect/pipes.php +++ b/tests/Features/Expect/pipes.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Pest\Exceptions\ExpectationNotFound; use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertEqualsIgnoringCase; use function PHPUnit\Framework\assertInstanceOf; From 266447bcc0ca00dfef11345878967d42a5b0ccf5 Mon Sep 17 00:00:00 2001 From: Owen Voke Date: Wed, 3 Nov 2021 09:59:51 +0000 Subject: [PATCH 45/45] feat: add `--memory` usage flag --- composer.json | 1 + src/Plugins/Memory.php | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/Plugins/Memory.php diff --git a/composer.json b/composer.json index 4d50f7b0..b5da2599 100644 --- a/composer.json +++ b/composer.json @@ -79,6 +79,7 @@ }, "pest": { "plugins": [ + "Pest\\Plugins\\Memory", "Pest\\Plugins\\Coverage", "Pest\\Plugins\\Init", "Pest\\Plugins\\Version", diff --git a/src/Plugins/Memory.php b/src/Plugins/Memory.php new file mode 100644 index 00000000..ffaf3358 --- /dev/null +++ b/src/Plugins/Memory.php @@ -0,0 +1,50 @@ +output = $output; + } + + public function handleArguments(array $arguments): array + { + foreach ($arguments as $index => $argument) { + if ($argument === '--memory') { + unset($arguments[$index]); + + $this->enabled = true; + } + } + + return array_values($arguments); + } + + public function addOutput(int $result): int + { + if ($this->enabled) { + $this->output->writeln(sprintf( + ' Memory: %s MB', + round(memory_get_usage(true) / pow(1000, 2), 3) + )); + } + + return $result; + } +}