From d802e8814843e39d07c0b78ffd02e0c7493602b6 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 7 Oct 2021 22:59:46 +0200 Subject: [PATCH 01/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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 fc2484a28a89e061ffa54ab3dca5879b1ae0d6dd Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 5 Nov 2021 12:00:56 +0100 Subject: [PATCH 11/31] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a4e616fe..e0925077 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ We would like to extend our thanks to the following sponsors for funding Pest development. If you are interested in becoming a sponsor, please visit the Nuno Maduro's [Sponsors page](https://github.com/sponsors/nunomaduro). +### Platinum Sponsors + +- **[Worksome](https://www.worksome.com/)** + ### Premium Sponsors - **[Akaunting](https://akaunting.com)** From 408ae4cad882feec1631445dc84b616c390237bf Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Mon, 8 Nov 2021 18:31:48 +0000 Subject: [PATCH 12/31] docs: adds `spatie.be` as platinum sponsor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0925077..e616d332 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ We would like to extend our thanks to the following sponsors for funding Pest de ### Platinum Sponsors +- **[Spatie](https://spatie.be)** - **[Worksome](https://www.worksome.com/)** ### Premium Sponsors @@ -31,6 +32,5 @@ We would like to extend our thanks to the following sponsors for funding Pest de - **[Fathom Analytics](https://usefathom.com/)** - **[Meema](https://meema.io)** - **[Scout APM](https://scoutapm.com)** -- **[Spatie](https://spatie.be)** Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. From da5c21de8f89808f2bff825282f29df7df538d52 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 9 Nov 2021 01:42:10 +0000 Subject: [PATCH 13/31] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e616d332..3f710c49 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ We would like to extend our thanks to the following sponsors for funding Pest de ### Premium Sponsors -- **[Akaunting](https://akaunting.com)** -- **[Auth0](https://auth0.com)** -- **[Codecourse](https://codecourse.com/)** -- **[Fathom Analytics](https://usefathom.com/)** -- **[Meema](https://meema.io)** -- **[Scout APM](https://scoutapm.com)** +- [Akaunting](https://akaunting.com) +- [Auth0](https://auth0.com) +- [Codecourse](https://codecourse.com/) +- [Fathom Analytics](https://usefathom.com/) +- [Meema](https://meema.io) +- [Scout APM](https://scoutapm.com) Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**. From 4b213d63bdcc45826f764e494c51bb1cc503e825 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 14 Nov 2021 19:58:25 +0000 Subject: [PATCH 14/31] feat: reworks evalution of Test Case --- .php-cs-fixer.dist.php | 1 + composer.json | 3 +- overrides/Runner/TestSuiteLoader.php | 122 ++++++--- src/Bootstrappers/BootEmitter.php | 29 -- src/Bootstrappers/BootExceptionHandler.php | 4 +- src/Bootstrappers/BootFiles.php | 3 +- src/Concerns/Testable.php | 81 +----- src/Datasets.php | 45 +++- src/Emitters/DispatchingEmitter.php | 251 ------------------ src/Factories/Annotations/Depends.php | 28 ++ src/Factories/Annotations/Groups.php | 25 ++ src/Factories/Concerns/HigherOrderable.php | 35 +++ src/Factories/TestCaseFactory.php | 205 ++++++++------ src/Factories/TestCaseMethodFactory.php | 102 +++++++ src/IgnorableTestCase.php | 15 ++ src/Kernel.php | 1 - src/Logging/TeamCity.php | 2 +- src/PendingCalls/TestCall.php | 42 +-- src/Pest.php | 2 +- src/Repositories/TestRepository.php | 157 +++++------ src/Subscribers/EnsureTestsAreLoaded.php | 3 + src/Support/HigherOrderMessage.php | 3 +- src/Support/Str.php | 10 + tests/Features/Datasets.php | 8 +- .../PHPUnit/CustomTestCase/CustomTestCase.php | 2 +- tests/Unit/TestSuite.php | 44 +-- 26 files changed, 603 insertions(+), 620 deletions(-) delete mode 100644 src/Bootstrappers/BootEmitter.php delete mode 100644 src/Emitters/DispatchingEmitter.php create mode 100644 src/Factories/Annotations/Depends.php create mode 100644 src/Factories/Annotations/Groups.php create mode 100644 src/Factories/Concerns/HigherOrderable.php create mode 100644 src/Factories/TestCaseMethodFactory.php create mode 100644 src/IgnorableTestCase.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index efa5d003..3454e972 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -3,6 +3,7 @@ $finder = PhpCsFixer\Finder::create() ->in(__DIR__ . DIRECTORY_SEPARATOR . 'tests') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'bin') + ->in(__DIR__ . DIRECTORY_SEPARATOR . 'overrides') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'stubs') ->in(__DIR__ . DIRECTORY_SEPARATOR . 'src') ->append(['.php-cs-fixer.dist.php']); diff --git a/composer.json b/composer.json index a4ef0856..1b8e7aaa 100644 --- a/composer.json +++ b/composer.json @@ -48,8 +48,7 @@ "illuminate/console": "^8.47.0", "illuminate/support": "^8.47.0", "laravel/dusk": "^6.15.0", - "pestphp/pest-dev-tools": "dev-master", - "pestphp/pest-plugin-mock": "^1.0" + "pestphp/pest-dev-tools": "dev-master" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/overrides/Runner/TestSuiteLoader.php b/overrides/Runner/TestSuiteLoader.php index 6535c0fa..19b37a82 100644 --- a/overrides/Runner/TestSuiteLoader.php +++ b/overrides/Runner/TestSuiteLoader.php @@ -1,22 +1,5 @@ . * All rights reserved. @@ -51,35 +34,82 @@ use PHPUnit\Framework\WarningTestCase; * POSSIBILITY OF SUCH DAMAGE. */ +declare(strict_types=1); + +namespace PHPUnit\Runner; + +use function array_diff; +use function array_values; +use function basename; +use function class_exists; +use function get_declared_classes; +use Pest\IgnorableTestCase; +use Pest\TestSuite; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionException; +use function stripos; +use function strlen; +use function substr; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ final class TestSuiteLoader { /** - * Loads the test suite. + * @psalm-var list + */ + private static array $loadedClasses = []; + + /** + * @psalm-var list + */ + private static array $declaredClasses = []; + + public function __construct() + { + if (empty(self::$declaredClasses)) { + self::$declaredClasses = get_declared_classes(); + } + } + + /** + * @throws Exception */ public function load(string $suiteClassFile): ReflectionClass { - $suiteClassName = basename($suiteClassFile, '.php'); - $loadedClasses = get_declared_classes(); + $suiteClassName = $this->classNameFromFileName($suiteClassFile); if (!class_exists($suiteClassName, false)) { (static function () use ($suiteClassFile) { include_once $suiteClassFile; + + TestSuite::getInstance()->tests->makeIfExists($suiteClassFile); })(); $loadedClasses = array_values( - array_diff(get_declared_classes(), $loadedClasses) + array_diff( + get_declared_classes(), + array_merge( + self::$declaredClasses, + self::$loadedClasses + ) + ) ); - if (empty($loadedClasses)) { - return new ReflectionClass(WarningTestCase::class); + self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses); + + if (empty(self::$loadedClasses)) { + return $this->exceptionFor($suiteClassName, $suiteClassFile); } } if (!class_exists($suiteClassName, false)) { + // this block will handle namespaced classes $offset = 0 - strlen($suiteClassName); - foreach ($loadedClasses as $loadedClass) { - + foreach (self::$loadedClasses as $loadedClass) { if (stripos(substr($loadedClass, $offset - 1), '\\' . $suiteClassName) === 0) { $suiteClassName = $loadedClass; @@ -89,18 +119,16 @@ final class TestSuiteLoader } if (!class_exists($suiteClassName, false)) { - return new ReflectionClass(WarningTestCase::class); + return $this->exceptionFor($suiteClassName, $suiteClassFile); } try { $class = new ReflectionClass($suiteClassName); + // @codeCoverageIgnoreStart } catch (ReflectionException $e) { - throw new Exception( - $e->getMessage(), - (int) $e->getCode(), - $e - ); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); } + // @codeCoverageIgnoreEnd if ($class->isSubclassOf(TestCase::class) && !$class->isAbstract()) { return $class; @@ -109,19 +137,39 @@ final class TestSuiteLoader if ($class->hasMethod('suite')) { try { $method = $class->getMethod('suite'); + // @codeCoverageIgnoreStart } catch (ReflectionException $e) { - throw new Exception( - $e->getMessage(), - (int) $e->getCode(), - $e - ); + throw new Exception($e->getMessage(), (int) $e->getCode(), $e); } + // @codeCoverageIgnoreEnd if (!$method->isAbstract() && $method->isPublic() && $method->isStatic()) { return $class; } } - return new ReflectionClass(WarningTestCase::class); + return $this->exceptionFor($suiteClassName, $suiteClassFile); + } + + public function reload(ReflectionClass $aClass): ReflectionClass + { + return $aClass; + } + + private function classNameFromFileName(string $suiteClassFile): string + { + $className = basename($suiteClassFile, '.php'); + $dotPos = strpos($className, '.'); + + if ($dotPos !== false) { + $className = substr($className, 0, $dotPos); + } + + return $className; + } + + private function exceptionFor(string $className, string $filename): ReflectionClass + { + return new ReflectionClass(IgnorableTestCase::class); } } diff --git a/src/Bootstrappers/BootEmitter.php b/src/Bootstrappers/BootEmitter.php deleted file mode 100644 index 0e389baf..00000000 --- a/src/Bootstrappers/BootEmitter.php +++ /dev/null @@ -1,29 +0,0 @@ -setStaticPropertyValue('emitter', new DispatchingEmitter( - $baseEmitter, - )); - } - } -} diff --git a/src/Bootstrappers/BootExceptionHandler.php b/src/Bootstrappers/BootExceptionHandler.php index 2494dc46..a504ee7e 100644 --- a/src/Bootstrappers/BootExceptionHandler.php +++ b/src/Bootstrappers/BootExceptionHandler.php @@ -16,6 +16,8 @@ final class BootExceptionHandler */ public function __invoke(): void { - (new Collision\Provider())->register(); + $handler = new Collision\Provider(); + + $handler->register(); } } diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php index 09958263..51781ff2 100644 --- a/src/Bootstrappers/BootFiles.php +++ b/src/Bootstrappers/BootFiles.php @@ -35,8 +35,7 @@ final class BootFiles */ public function __invoke(): void { - $rootPath = TestSuite::getInstance()->rootPath; - + $rootPath = TestSuite::getInstance()->rootPath; $testsPath = $rootPath . DIRECTORY_SEPARATOR . testDirectory(); foreach (self::STRUCTURE as $filename) { diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index d90229cd..76e8bc88 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -5,11 +5,9 @@ declare(strict_types=1); namespace Pest\Concerns; use Closure; -use Pest\Support\Backtrace; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\TestSuite; -use PHPUnit\Framework\ExecutionOrderDependency; use Throwable; /** @@ -17,11 +15,6 @@ use Throwable; */ trait Testable { - /** - * The Test Case description. - */ - private string $__description; - /** * The Test Case "test" closure. */ @@ -47,47 +40,23 @@ trait Testable */ private static ?Closure $__afterAll = null; + /** + * Resets the test case static properties. + */ + public static function flush(): void + { + self::$__beforeAll = null; + self::$__afterAll = null; + } + /** * Creates a new Test Case instance. */ - public function __construct(Closure $test, string $description, array $data) + public function __construct(string $name) { - $this->__test = $test; - $this->__description = $description; - self::$__beforeAll = null; - self::$__afterAll = null; + parent::__construct($name); - parent::__construct('__test'); - - $this->setData($description, $data); - } - - /** - * Adds groups to the Test Case. - */ - public function addGroups(array $groups): void - { - $groups = array_unique(array_merge($this->groups(), $groups)); - - $this->setGroups($groups); - } - - /** - * Adds dependencies to the Test Case. - */ - public function addDependencies(array $tests): void - { - $className = $this::class; - - $tests = array_map(static function (string $test) use ($className): ExecutionOrderDependency { - if (!str_contains($test, '::')) { - $test = "{$className}::{$test}"; - } - - return new ExecutionOrderDependency($test, '__test'); - }, $tests); - - $this->setDependencies($tests); + $this->__test = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($name)->getClosure($this); } /** @@ -148,16 +117,6 @@ trait Testable : $hook; } - /** - * Gets the Test Case name. - */ - public function getName(bool $withDataSet = true): string - { - return (str_ends_with(Backtrace::file(), 'TestRunner.php') || Backtrace::line() === 277) - ? '__test' - : $this->__description; - } - /** * Gets the Test Case filename. */ @@ -234,26 +193,14 @@ trait Testable TestSuite::getInstance()->test = null; } - /** - * Gets the Test Case filename and description. - */ - public function toString(): string - { - return \sprintf( - '%s::%s', - self::$__filename, - $this->__description - ); - } - /** * Executes the Test Case current test. * * @throws Throwable */ - public function __test(): mixed + private function __runTest(Closure $closure, ...$args): mixed { - return $this->__callClosure($this->__test, $this->__resolveTestArguments(func_get_args())); + return $this->__callClosure($closure, $this->__resolveTestArguments($args)); } /** diff --git a/src/Datasets.php b/src/Datasets.php index 0a245079..38cc6662 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -22,6 +22,13 @@ final class Datasets */ private static array $datasets = []; + /** + * Holds the withs. + * + * @var array + */ + private static array $withs = []; + /** * Sets the given. * @@ -36,35 +43,43 @@ final class Datasets self::$datasets[$name] = $data; } + /** + * Sets the given. + * + * @param Closure|iterable $data + */ + public static function with(string $filename, string $description, Closure|iterable|string $with): void + { + self::$withs[$filename . '>>>' . $description] = $with; + } + /** * @return Closure|iterable */ - public static function get(string $name): Closure|iterable + public static function get(string $filename, string $description): Closure|iterable { - if (!array_key_exists($name, self::$datasets)) { - throw new DatasetDoesNotExist($name); - } + $dataset = self::$withs[$filename . '>>>' . $description]; - return self::$datasets[$name]; + return self::resolve($description, $dataset); } /** * Resolves the current dataset to an array value. * - * @param array|string> $datasets + * @param array|string> $dataset * - * @return array + * @return array|null */ - public static function resolve(string $description, array $datasets): array + public static function resolve(string $description, array $dataset): array|null { /* @phpstan-ignore-next-line */ - if (empty($datasets)) { - return [$description => []]; + if (empty($dataset)) { + return null; } - $datasets = self::processDatasets($datasets); + $dataset = self::processDatasets($dataset); - $datasetCombinations = self::getDataSetsCombinations($datasets); + $datasetCombinations = self::getDataSetsCombinations($dataset); $dataSetDescriptions = []; $dataSetValues = []; @@ -114,7 +129,11 @@ final class Datasets $processedDataset = []; if (is_string($data)) { - $datasets[$index] = self::get($data); + if (!isset(self::$datasets[$data])) { + throw new DatasetDoesNotExist($data); + } + + $datasets[$index] = self::$datasets[$data]; } if (is_callable($datasets[$index])) { diff --git a/src/Emitters/DispatchingEmitter.php b/src/Emitters/DispatchingEmitter.php deleted file mode 100644 index d8e82fda..00000000 --- a/src/Emitters/DispatchingEmitter.php +++ /dev/null @@ -1,251 +0,0 @@ -baseEmitter->eventFacadeSealed(...func_get_args()); - } - - public function testRunnerStarted(): void - { - $this->baseEmitter->testRunnerStarted(...func_get_args()); - } - - public function testRunnerConfigured(Configuration $configuration): void - { - $this->baseEmitter->testRunnerConfigured($configuration); - } - - public function testRunnerFinished(): void - { - $this->baseEmitter->testRunnerFinished(...func_get_args()); - } - - public function assertionMade(mixed $value, Constraint\Constraint $constraint, string $message, bool $hasFailed): void - { - $this->baseEmitter->assertionMade($value, $constraint, $message, $hasFailed); - } - - public function bootstrapFinished(string $filename): void - { - $this->baseEmitter->bootstrapFinished($filename); - } - - public function comparatorRegistered(string $className): void - { - $this->baseEmitter->comparatorRegistered($className); - } - - public function extensionLoaded(string $name, string $version): void - { - $this->baseEmitter->extensionLoaded($name, $version); - } - - public function globalStateCaptured(Snapshot $snapshot): void - { - $this->baseEmitter->globalStateCaptured($snapshot); - } - - public function globalStateModified(Snapshot $snapshotBefore, Snapshot $snapshotAfter, string $diff): void - { - $this->baseEmitter->globalStateModified($snapshotBefore, $snapshotAfter, $diff); - } - - public function globalStateRestored(Snapshot $snapshot): void - { - $this->baseEmitter->globalStateRestored($snapshot); - } - - public function testErrored(Code\Test $test, Throwable $throwable): void - { - $this->baseEmitter->testErrored(...func_get_args()); - } - - public function testFailed(Code\Test $test, Throwable $throwable): void - { - $this->baseEmitter->testFailed(...func_get_args()); - } - - public function testFinished(Code\Test $test): void - { - $this->baseEmitter->testFinished(...func_get_args()); - } - - public function testOutputPrinted(Code\Test $test, string $output): void - { - $this->baseEmitter->testOutputPrinted(...func_get_args()); - } - - public function testPassed(Code\Test $test): void - { - $this->baseEmitter->testPassed(...func_get_args()); - } - - public function testPassedWithWarning(Code\Test $test, Throwable $throwable): void - { - $this->baseEmitter->testPassedWithWarning(...func_get_args()); - } - - public function testConsideredRisky(Code\Test $test, Throwable $throwable): void - { - $this->baseEmitter->testConsideredRisky(...func_get_args()); - } - - public function testAborted(Code\Test $test, Throwable $throwable): void - { - $this->baseEmitter->testAborted(...func_get_args()); - } - - public function testSkipped(Code\Test $test, string $message): void - { - $this->baseEmitter->testSkipped(...func_get_args()); - } - - public function testPrepared(Code\Test $test): void - { - $this->baseEmitter->testPrepared(...func_get_args()); - } - - public function testAfterTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void - { - $this->baseEmitter->testAfterTestMethodFinished(...func_get_args()); - } - - public function testAfterLastTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void - { - $this->baseEmitter->testAfterLastTestMethodFinished(...func_get_args()); - } - - public function testBeforeFirstTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void - { - $this->baseEmitter->testBeforeFirstTestMethodCalled(...func_get_args()); - } - - public function testBeforeFirstTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void - { - $this->baseEmitter->testBeforeFirstTestMethodFinished(...func_get_args()); - } - - public function testBeforeTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void - { - $this->baseEmitter->testBeforeTestMethodCalled(...func_get_args()); - } - - public function testBeforeTestMethodFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void - { - $this->baseEmitter->testBeforeTestMethodFinished(...func_get_args()); - } - - public function testPreConditionCalled(string $testClassName, Code\ClassMethod $calledMethod): void - { - $this->baseEmitter->testPreConditionCalled(...func_get_args()); - } - - public function testPreConditionFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void - { - $this->baseEmitter->testPreConditionFinished(...func_get_args()); - } - - public function testPostConditionCalled(string $testClassName, Code\ClassMethod $calledMethod): void - { - $this->baseEmitter->testPostConditionCalled(...func_get_args()); - } - - public function testPostConditionFinished(string $testClassName, Code\ClassMethod ...$calledMethods): void - { - $this->baseEmitter->testPostConditionFinished(...func_get_args()); - } - - public function testAfterTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void - { - $this->baseEmitter->testAfterTestMethodCalled(...func_get_args()); - } - - public function testAfterLastTestMethodCalled(string $testClassName, Code\ClassMethod $calledMethod): void - { - $this->baseEmitter->testAfterLastTestMethodCalled(...func_get_args()); - } - - public function testMockObjectCreated(string $className): void - { - $this->baseEmitter->testMockObjectCreated(...func_get_args()); - } - - public function testMockObjectCreatedForTrait(string $traitName): void - { - $this->baseEmitter->testMockObjectCreatedForTrait(...func_get_args()); - } - - public function testMockObjectCreatedForAbstractClass(string $className): void - { - $this->baseEmitter->testMockObjectCreatedForAbstractClass(...func_get_args()); - } - - public function testMockObjectCreatedFromWsdl(string $wsdlFile, string $originalClassName, string $mockClassName, array $methods, bool $callOriginalConstructor, array $options): void - { - $this->baseEmitter->testMockObjectCreatedFromWsdl(...func_get_args()); - } - - public function testPartialMockObjectCreated(string $className, string ...$methodNames): void - { - $this->baseEmitter->testPartialMockObjectCreated(...func_get_args()); - } - - public function testTestProxyCreated(string $className, array $constructorArguments): void - { - $this->baseEmitter->testTestProxyCreated(...func_get_args()); - } - - public function testTestStubCreated(string $className): void - { - $this->baseEmitter->testTestStubCreated(...func_get_args()); - } - - public function testSuiteLoaded(TestSuite $testSuite): void - { - EnsureTestsAreLoaded::setTestSuite($testSuite); - - $this->baseEmitter->testSuiteLoaded(...func_get_args()); - } - - public function testSuiteSorted(int $executionOrder, int $executionOrderDefects, bool $resolveDependencies): void - { - $this->baseEmitter->testSuiteSorted(...func_get_args()); - } - - public function testSuiteStarted(TestSuite $testSuite): void - { - $this->baseEmitter->testSuiteStarted(...func_get_args()); - } - - public function testSuiteFinished(TestSuite $testSuite, TestResult $result): void - { - $this->baseEmitter->testSuiteFinished(...func_get_args()); - } -} diff --git a/src/Factories/Annotations/Depends.php b/src/Factories/Annotations/Depends.php new file mode 100644 index 00000000..15b29359 --- /dev/null +++ b/src/Factories/Annotations/Depends.php @@ -0,0 +1,28 @@ +depends as $depend) { + $depend = Str::evaluable($depend); + + $annotations[] = "@depends $depend"; + } + + return $annotations; + } +} diff --git a/src/Factories/Annotations/Groups.php b/src/Factories/Annotations/Groups.php new file mode 100644 index 00000000..96752d6e --- /dev/null +++ b/src/Factories/Annotations/Groups.php @@ -0,0 +1,25 @@ +groups as $group) { + $annotations[] = "@group $group"; + } + + return $annotations; + } +} diff --git a/src/Factories/Concerns/HigherOrderable.php b/src/Factories/Concerns/HigherOrderable.php new file mode 100644 index 00000000..160d8684 --- /dev/null +++ b/src/Factories/Concerns/HigherOrderable.php @@ -0,0 +1,35 @@ +chains = new HigherOrderMessageCollection(); + $this->factoryProxies = new HigherOrderMessageCollection(); + $this->proxies = new HigherOrderMessageCollection(); + } +} diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 4863552a..8882a0da 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -4,16 +4,18 @@ declare(strict_types=1); namespace Pest\Factories; -use Closure; use ParseError; use Pest\Concerns; use Pest\Contracts\HasPrintableTestCaseName; use Pest\Datasets; +use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\ShouldNotHappen; -use Pest\Support\HigherOrderMessageCollection; +use Pest\Exceptions\TestAlreadyExist; +use Pest\Factories\Concerns\HigherOrderable; +use Pest\Plugins\Environment; +use Pest\Support\Reflection; use Pest\Support\Str; use Pest\TestSuite; -use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -22,22 +24,17 @@ use RuntimeException; */ final class TestCaseFactory { - /** - * Determines if the Test Case will be the "only" being run. - */ - public bool $only = false; + use HigherOrderable; /** - * The Test Case closure. - */ - public Closure $test; - - /** - * The Test Case Dataset, if any. + * The list of annotations. * - * @var array|string> + * @var array */ - public array $datasets = []; + private static array $annotations = [ + Annotations\Depends::class, + Annotations\Groups::class, + ]; /** * The FQN of the Test Case class. @@ -47,7 +44,14 @@ final class TestCaseFactory public string $class = TestCase::class; /** - * An array of FQN of the Test Case traits. + * The list of class methods. + * + * @var array + */ + public array $methods = []; + + /** + * The list of class traits. * * @var array */ @@ -56,81 +60,48 @@ final class TestCaseFactory Concerns\Expectable::class, ]; - /** - * The higher order messages for the factory that are proxyable. - */ - public HigherOrderMessageCollection $factoryProxies; - - /** - * The higher order messages that are proxyable. - */ - public HigherOrderMessageCollection $proxies; - - /** - * The higher order messages that are chainable. - */ - public HigherOrderMessageCollection $chains; - /** * Creates a new Factory instance. */ public function __construct( - public string $filename, - public ?string $description, - Closure $closure = null) - { - $this->test = $closure ?? fn () => Assert::getCount() > 0 ?: self::markTestIncomplete(); + public string $filename + ) { + $this->bootHigherOrderable(); + } - $this->factoryProxies = new HigherOrderMessageCollection(); - $this->proxies = new HigherOrderMessageCollection(); - $this->chains = new HigherOrderMessageCollection(); + public function make(): void + { + $methods = array_filter($this->methods, function ($method) { + return count($onlyTestCases = $this->methodsUsingOnly()) === 0 || in_array($method, $onlyTestCases, true); + }); + + if (count($this->methods) > 0) { + $this->evaluate($this->filename, $methods); + } } /** - * Makes the Test Case classes. + * Returns all the "only" methods. * - * @return array + * @return array */ - public function make(): array + public function methodsUsingOnly(): array { - if ($this->description === null) { - throw ShouldNotHappen::fromMessage('Description can not be empty.'); + if (Environment::name() === Environment::CI) { + return []; } - $chains = $this->chains; - $proxies = $this->proxies; - $factoryTest = $this->test; - - $testClosure = function () use ($chains, $proxies, $factoryTest): mixed { - $proxies->proxy($this); - $chains->chain($this); - - /* @phpstan-ignore-next-line */ - return call_user_func(Closure::bind($factoryTest, $this, $this::class), ...func_get_args()); - }; - - $className = $this->makeClassFromFilename($this->filename); - - $createTest = function ($description, $data) use ($className, $testClosure) { - $testCase = new $className($testClosure, $description, $data); - $this->factoryProxies->proxy($testCase); - - return $testCase; - }; - - $datasets = Datasets::resolve($this->description, $this->datasets); - - return array_map($createTest, array_keys($datasets), $datasets); + return array_filter($this->methods, static fn ($method): bool => $method->only); } /** - * Makes a Fully Qualified Class Name from the given filename. + * Creates a Test Case class using a runtime evaluate. */ - public function makeClassFromFilename(string $filename): string + public function evaluate(string $filename, array $methods): string { if ('\\' === DIRECTORY_SEPARATOR) { // In case Windows, strtolower drive name, like in UsesCall. - $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', fn ($match): string => strtolower($match['drive']), $filename); + $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $filename); } $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename))); @@ -152,7 +123,9 @@ final class TestCaseFactory } $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); - $traitsCode = sprintf('use %s;', implode(', ', array_map(fn ($trait): string => sprintf('\%s', $trait), $this->traits))); + $traitsCode = sprintf('use %s;', implode(', ', array_map( + static fn ($trait): string => sprintf('\%s', $trait), $this->traits)) + ); $partsFQN = explode('\\', $classFQN); $className = array_pop($partsFQN); @@ -164,14 +137,65 @@ final class TestCaseFactory $classFQN .= $className; } + $methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string { + $methodName = Str::evaluable($method->description); + + $datasetsCode = ''; + $annotations = ['@test']; + + foreach (self::$annotations as $annotation) { + $annotations = (new $annotation())->add($method, $annotations); + } + + if (!empty($method->datasets)) { + $dataProviderName = $methodName . '_dataset'; + $annotations[] = "@dataProvider $dataProviderName"; + + Datasets::with($method->filename, $methodName, $method->datasets); + + $datasetsCode = << sprintf("\n * %s", $annotation), $annotations, + )); + + return <<__runTest( + \$this->__test, + ...func_get_args(), + ); + } + + $datasetsCode +EOF; + }, $methods)); + try { eval(" namespace $namespace; + use Pest\Datasets as __PestDatasets; + use Pest\TestSuite as __PestTestSuite; + final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { $traitsCode private static \$__filename = '$filename'; + + $methodsCode } "); } catch (ParseError $caught) { @@ -182,11 +206,40 @@ final class TestCaseFactory } /** - * Determine if the test case will receive argument input from Pest, or not. + * Adds the given Method to the Test Case. */ - public function __receivesArguments(): bool + public function addMethod(TestCaseMethodFactory $method): void { - return count($this->datasets) > 0 - || $this->factoryProxies->count('addDependencies') > 0; + if ($method->description === null) { + throw ShouldNotHappen::fromMessage('The test description may not be empty.'); + } + + if (isset($this->methods[$method->description])) { + throw new TestAlreadyExist($method->filename, $method->description); + } + + if (!$method->receivesArguments()) { + $arguments = Reflection::getFunctionArguments($method->closure); + + if (count($arguments) > 0) { + throw new DatasetMissing($method->filename, $method->description, $arguments); + } + } + + $this->methods[$method->description] = $method; + } + + /** + * Gets a Method by the given name. + */ + public function getMethod(string $methodName): TestCaseMethodFactory + { + foreach ($this->methods as $method) { + if (Str::evaluable($method->description) === $methodName) { + return $method; + } + } + + throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName)); } } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php new file mode 100644 index 00000000..dd32fceb --- /dev/null +++ b/src/Factories/TestCaseMethodFactory.php @@ -0,0 +1,102 @@ +|string> + */ + public array $datasets = []; + + /** + * The Test Case depends, if any. + * + * @var array + */ + public array $depends = []; + + /** + * The Test Case groups, if any. + * + * @var array + */ + public array $groups = []; + + /** + * Creates a new Factory instance. + */ + public function __construct( + public string $filename, + public ?string $description, + public ?Closure $closure, + ) { + if ($this->closure === null) { + $this->closure = function () { + Assert::getCount() > 0 ?: self::markTestIncomplete(); + }; + } + + $this->bootHigherOrderable(); + } + + /** + * Makes the Test Case classes. + */ + public function getClosure(TestCase $concrete): Closure + { + $concrete::flush(); + + if ($this->description === null) { + throw ShouldNotHappen::fromMessage('Description can not be empty.'); + } + + $closure = $this->closure; + + $testCase = TestSuite::getInstance()->tests->get($this->filename); + + $testCase->factoryProxies->proxy($concrete); + $this->factoryProxies->proxy($concrete); + + $method = $this; + + return function () use ($testCase, $method, $closure): mixed { + $testCase->proxies->proxy($this); + $method->proxies->proxy($this); + + $testCase->chains->chain($this); + $method->chains->chain($this); + + return call_user_func(Closure::bind($closure, $this, $this::class), ...func_get_args()); + }; + } + + /** + * Determine if the test case will receive argument input from Pest, or not. + */ + public function receivesArguments(): bool + { + return count($this->datasets) > 0 || count($this->depends) > 0; + } +} diff --git a/src/IgnorableTestCase.php b/src/IgnorableTestCase.php new file mode 100644 index 00000000..ba4a4bf1 --- /dev/null +++ b/src/IgnorableTestCase.php @@ -0,0 +1,15 @@ +phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors); diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index f6c1300b..25c6426e 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Pest\PendingCalls; use Closure; -use Pest\Factories\TestCaseFactory; +use Pest\Factories\TestCaseMethodFactory; use Pest\Support\Backtrace; use Pest\Support\HigherOrderCallables; use Pest\Support\NullClosure; @@ -22,7 +22,7 @@ final class TestCall /** * The Test Case Factory. */ - private TestCaseFactory $testCaseFactory; + private TestCaseMethodFactory $testCaseMethod; /** * If test call is descriptionLess. @@ -38,7 +38,7 @@ final class TestCall string $description = null, Closure $closure = null ) { - $this->testCaseFactory = new TestCaseFactory($filename, $description, $closure); + $this->testCaseMethod = new TestCaseMethodFactory($filename, $description, $closure); $this->descriptionLess = $description === null; } @@ -48,7 +48,7 @@ final class TestCall public function throws(string $exception, string $exceptionMessage = null): TestCall { if (class_exists($exception)) { - $this->testCaseFactory + $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectException', [$exception]); } else { @@ -56,7 +56,7 @@ final class TestCall } if (is_string($exceptionMessage)) { - $this->testCaseFactory + $this->testCaseMethod ->proxies ->add(Backtrace::file(), Backtrace::line(), 'expectExceptionMessage', [$exceptionMessage]); } @@ -90,10 +90,10 @@ final class TestCall * * @param array<\Closure|iterable|string> $data */ - public function with(...$data): TestCall + public function with(Closure|iterable|string ...$data): TestCall { foreach ($data as $dataset) { - $this->testCaseFactory->datasets[] = $dataset; + $this->testCaseMethod->datasets[] = $dataset; } return $this; @@ -102,11 +102,11 @@ final class TestCall /** * Sets the test depends. */ - public function depends(string ...$tests): TestCall + public function depends(string ...$depends): TestCall { - $this->testCaseFactory - ->factoryProxies - ->add(Backtrace::file(), Backtrace::line(), 'addDependencies', [$tests]); + foreach ($depends as $depend) { + $this->testCaseMethod->depends[] = $depend; + } return $this; } @@ -116,7 +116,7 @@ final class TestCall */ public function only(): TestCall { - $this->testCaseFactory->only = true; + $this->testCaseMethod->only = true; return $this; } @@ -126,9 +126,9 @@ final class TestCall */ public function group(string ...$groups): TestCall { - $this->testCaseFactory - ->factoryProxies - ->add(Backtrace::file(), Backtrace::line(), 'addGroups', [$groups]); + foreach ($groups as $group) { + $this->testCaseMethod->groups[] = $group; + } return $this; } @@ -153,7 +153,7 @@ final class TestCall /** @var callable(): bool $condition */ $condition = $condition->bindTo(null); - $this->testCaseFactory + $this->testCaseMethod ->chains ->addWhen($condition, Backtrace::file(), Backtrace::line(), 'markTestSkipped', [$message]); @@ -185,16 +185,16 @@ final class TestCall */ private function addChain(string $name, array $arguments = null): self { - $this->testCaseFactory + $this->testCaseMethod ->chains ->add(Backtrace::file(), Backtrace::line(), $name, $arguments); if ($this->descriptionLess) { $exporter = new Exporter(); - if ($this->testCaseFactory->description !== null) { - $this->testCaseFactory->description .= ' → '; + if ($this->testCaseMethod->description !== null) { + $this->testCaseMethod->description .= ' → '; } - $this->testCaseFactory->description .= $arguments === null + $this->testCaseMethod->description .= $arguments === null ? $name : sprintf('%s %s', $name, $exporter->shortenedRecursiveExport($arguments)); } @@ -207,6 +207,6 @@ final class TestCall */ public function __destruct() { - $this->testSuite->tests->set($this->testCaseFactory); + $this->testSuite->tests->set($this->testCaseMethod); } } diff --git a/src/Pest.php b/src/Pest.php index 56a43e84..e6b0e1f7 100644 --- a/src/Pest.php +++ b/src/Pest.php @@ -6,7 +6,7 @@ namespace Pest; function version(): string { - return '1.20.0'; + return '2.x-dev'; } function testDirectory(string $file = ''): string diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 40c2972b..57d8e12c 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -5,16 +5,11 @@ declare(strict_types=1); namespace Pest\Repositories; use Closure; -use Pest\Exceptions\DatasetMissing; -use Pest\Exceptions\ShouldNotHappen; -use Pest\Exceptions\TestAlreadyExist; use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseClassOrTraitNotFound; use Pest\Factories\TestCaseFactory; -use Pest\Plugins\Environment; -use Pest\Support\Reflection; +use Pest\Factories\TestCaseMethodFactory; use Pest\Support\Str; -use Pest\TestSuite; use PHPUnit\Framework\TestCase; /** @@ -22,15 +17,10 @@ use PHPUnit\Framework\TestCase; */ final class TestRepository { - /** - * @var non-empty-string - */ - private const SEPARATOR = '>>>'; - /** * @var array */ - private array $state = []; + private array $testCases = []; /** * @var array>> @@ -42,7 +32,7 @@ final class TestRepository */ public function count(): int { - return count($this->state); + return count($this->testCases); } /** @@ -52,74 +42,13 @@ final class TestRepository */ public function getFilenames(): array { - $testsWithOnly = $this->testsUsingOnly(); + $testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase) => count($testCase->methodsUsingOnly()) > 0); - return array_values(array_map(fn (TestCaseFactory $factory): string => $factory->filename, count($testsWithOnly) > 0 ? $testsWithOnly : $this->state)); - } - - /** - * Calls the given callable foreach test case. - */ - public function build(TestSuite $testSuite, callable $each): void - { - $startsWith = fn (string $target, string $directory): bool => Str::startsWith($target, $directory . DIRECTORY_SEPARATOR); - - foreach ($this->uses as $path => $uses) { - [$classOrTraits, $groups, $hooks] = $uses; - - $setClassName = function (TestCaseFactory $testCase, string $key) use ($path, $classOrTraits, $groups, $startsWith, $hooks): void { - [$filename] = explode(self::SEPARATOR, $key); - - if ((!is_dir($path) && $filename === $path) || (is_dir($path) && $startsWith($filename, $path))) { - foreach ($classOrTraits as $class) { /** @var string $class */ - if (class_exists($class)) { - if ($testCase->class !== TestCase::class) { - throw new TestCaseAlreadyInUse($testCase->class, $class, $filename); - } - $testCase->class = $class; - } elseif (trait_exists($class)) { - $testCase->traits[] = $class; - } - } - - $testCase->factoryProxies->add($filename, 0, 'addGroups', [$groups]); - $testCase->factoryProxies->add($filename, 0, '__addBeforeAll', [$hooks[0] ?? null]); - $testCase->factoryProxies->add($filename, 0, '__addBeforeEach', [$hooks[1] ?? null]); - $testCase->factoryProxies->add($filename, 0, '__addAfterEach', [$hooks[2] ?? null]); - $testCase->factoryProxies->add($filename, 0, '__addAfterAll', [$hooks[3] ?? null]); - } - }; - - foreach ($this->state as $key => $test) { - $setClassName($test, $key); - } + if (count($testCases) === 0) { + $testCases = $this->testCases; } - $onlyState = $this->testsUsingOnly(); - - $state = count($onlyState) > 0 ? $onlyState : $this->state; - - foreach ($state as $testFactory) { - /** @var TestCaseFactory $testFactory */ - $tests = $testFactory->make($testSuite); - foreach ($tests as $test) { - $each($test); - } - } - } - - /** - * Return all tests that have called the only method. - * - * @return array - */ - private function testsUsingOnly(): array - { - if (Environment::name() === Environment::CI) { - return []; - } - - return array_filter($this->state, fn ($testFactory): bool => $testFactory->only); + return array_values(array_map(static fn (TestCaseFactory $factory): string => $factory->filename, $testCases)); } /** @@ -151,27 +80,73 @@ final class TestRepository } } - /** - * Sets a test case by the given filename and description. - */ - public function set(TestCaseFactory $test): void + public function get($filename): TestCaseFactory { - if ($test->description === null) { - throw ShouldNotHappen::fromMessage('Trying to create a test without description.'); + return $this->testCases[$filename]; + } + + /** + * Sets a new test case method. + */ + public function set(TestCaseMethodFactory $method): void + { + if (!isset($this->testCases[$method->filename])) { + $this->testCases[$method->filename] = new TestCaseFactory($method->filename); } - if (array_key_exists(sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description), $this->state)) { - throw new TestAlreadyExist($test->filename, $test->description); + $this->testCases[$method->filename]->addMethod($method); + } + + /** + * Makes a Test Case from the given filename, if exists. + */ + public function makeIfExists(string $filename): void + { + if (isset($this->testCases[$filename])) { + $this->make($this->testCases[$filename]); } + } - if (!$test->__receivesArguments()) { - $arguments = Reflection::getFunctionArguments($test->test); + /** + * Makes a Test Case using the given factory. + */ + private function make(TestCaseFactory $testCase): void + { + $startsWith = static fn (string $target, string $directory): bool => Str::startsWith($target, $directory . DIRECTORY_SEPARATOR); - if (count($arguments) > 0) { - throw new DatasetMissing($test->filename, $test->description, $arguments); + foreach ($this->uses as $path => $uses) { + [$classOrTraits, $groups, $hooks] = $uses; + + if ((!is_dir($path) && $testCase->filename === $path) || (is_dir($path) && $startsWith($testCase->filename, $path))) { + foreach ($classOrTraits as $class) { + /** @var string $class */ + if (class_exists($class)) { + if ($testCase->class !== TestCase::class) { + throw new TestCaseAlreadyInUse($testCase->class, $class, $testCase->filename); + } + $testCase->class = $class; + } elseif (trait_exists($class)) { + $testCase->traits[] = $class; + } + } + + foreach ($testCase->methods as $method) { + foreach ($groups as $group) { + $method->groups[] = $group; + } + } + + foreach ($testCase->methods as $method) { + $method->groups = array_merge($groups, $method->groups); + } + + $testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeAll', [$hooks[0] ?? null]); + $testCase->factoryProxies->add($testCase->filename, 0, '__addBeforeEach', [$hooks[1] ?? null]); + $testCase->factoryProxies->add($testCase->filename, 0, '__addAfterEach', [$hooks[2] ?? null]); + $testCase->factoryProxies->add($testCase->filename, 0, '__addAfterAll', [$hooks[3] ?? null]); } } - $this->state[sprintf('%s%s%s', $test->filename, self::SEPARATOR, $test->description)] = $test; + $testCase->make(); } } diff --git a/src/Subscribers/EnsureTestsAreLoaded.php b/src/Subscribers/EnsureTestsAreLoaded.php index 12396941..23954c24 100644 --- a/src/Subscribers/EnsureTestsAreLoaded.php +++ b/src/Subscribers/EnsureTestsAreLoaded.php @@ -25,6 +25,8 @@ final class EnsureTestsAreLoaded implements LoadedSubscriber */ public function notify(Loaded $event): void { + /* + $this->removeWarnings(self::$testSuite); $testSuites = []; @@ -47,6 +49,7 @@ final class EnsureTestsAreLoaded implements LoadedSubscriber } self::$testSuite->addTestSuite($testTestSuite); } + */ } /** diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index 99f444f0..1df45e4e 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -54,8 +54,7 @@ final class HigherOrderMessage try { return is_array($this->arguments) ? Reflection::call($target, $this->name, $this->arguments) - : $target->{$this->name}; - /* @phpstan-ignore-line */ + : $target->{$this->name}; /* @phpstan-ignore-line */ } catch (Throwable $throwable) { Reflection::setPropertyValue($throwable, 'file', $this->filename); Reflection::setPropertyValue($throwable, 'line', $this->line); diff --git a/src/Support/Str.php b/src/Support/Str.php index a3b7a3ab..a7217704 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -48,4 +48,14 @@ final class Str return substr($target, -$length) === $search; } + + /** + * Makes the given string evaluable by an `eval`. + */ + public static function evaluable(string $code): string + { + $code = str_replace(' ', '_', $code); + + return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code); + } } diff --git a/tests/Features/Datasets.php b/tests/Features/Datasets.php index be94e596..753c4eaf 100644 --- a/tests/Features/Datasets.php +++ b/tests/Features/Datasets.php @@ -12,7 +12,8 @@ beforeEach(function () { it('throws exception if dataset does not exist', function () { $this->expectException(DatasetDoesNotExist::class); $this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`."); - Datasets::get('first'); + + Datasets::resolve('foo', ['first']); }); it('throws exception if dataset already exist', function () { @@ -27,13 +28,13 @@ it('sets closures', function () { yield [1]; }); - expect(iterator_to_array(Datasets::get('foo')()))->toBe([[1]]); + expect(Datasets::resolve('foo', ['foo']))->toBe(['foo with (1)' => [1]]); }); it('sets arrays', function () { Datasets::set('bar', [[2]]); - expect(Datasets::get('bar'))->toBe([[2]]); + expect(Datasets::resolve('bar', ['bar']))->toBe(['bar with (2)' => [2]]); }); it('gets bound to test case object', function () { @@ -52,6 +53,7 @@ $datasets = [[1], [2]]; test('lazy datasets', function ($text) use ($state, $datasets) { $state->text .= $text; + expect(in_array([$text], $datasets))->toBe(true); })->with($datasets); diff --git a/tests/PHPUnit/CustomTestCase/CustomTestCase.php b/tests/PHPUnit/CustomTestCase/CustomTestCase.php index 2eb0ad4e..8a71327b 100644 --- a/tests/PHPUnit/CustomTestCase/CustomTestCase.php +++ b/tests/PHPUnit/CustomTestCase/CustomTestCase.php @@ -7,7 +7,7 @@ namespace Tests\CustomTestCase; use function PHPUnit\Framework\assertTrue; use PHPUnit\Framework\TestCase; -class CustomTestCase extends TestCase +abstract class CustomTestCase extends TestCase { public function assertCustomTrue() { diff --git a/tests/Unit/TestSuite.php b/tests/Unit/TestSuite.php index 84212879..a15ad631 100644 --- a/tests/Unit/TestSuite.php +++ b/tests/Unit/TestSuite.php @@ -2,53 +2,55 @@ use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\TestAlreadyExist; -use Pest\Factories\TestCaseFactory; +use Pest\Factories\TestCaseMethodFactory; use Pest\Plugins\Environment; use Pest\TestSuite; it('does not allow to add the same test description twice', function () { $testSuite = new TestSuite(getcwd(), 'tests'); - $test = function () {}; - $testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); - $testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); + $method = new TestCaseMethodFactory('foo', 'bar', null); + + $testSuite->tests->set($method); + $testSuite->tests->set($method); })->throws( TestAlreadyExist::class, - sprintf('A test with the description `%s` already exist in the filename `%s`.', 'foo', __FILE__), + sprintf('A test with the description `%s` already exist in the filename `%s`.', 'bar', 'foo'), ); it('alerts users about tests with arguments but no input', function () { $testSuite = new TestSuite(getcwd(), 'tests'); - $test = function (int $arg) {}; - $testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); + + $method = new TestCaseMethodFactory('foo', 'bar', function (int $arg) {}); + + $testSuite->tests->set($method); })->throws( DatasetMissing::class, - sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'foo', 1, 'int $arg', __FILE__), + sprintf("A test with the description '%s' has %d argument(s) ([%s]) and no dataset(s) provided in %s", 'bar', 1, 'int $arg', 'foo'), ); it('can return an array of all test suite filenames', function () { $testSuite = TestSuite::getInstance(getcwd(), 'tests'); - $test = function () {}; - $testSuite->tests->set(new TestCaseFactory(__FILE__, 'foo', $test)); - $testSuite->tests->set(new TestCaseFactory(__FILE__, 'bar', $test)); + + $testSuite->tests->set(new TestCaseMethodFactory('a', 'b', null)); + $testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null)); expect($testSuite->tests->getFilenames())->toEqual([ - __FILE__, - __FILE__, + 'a', + 'c', ]); }); it('can filter the test suite filenames to those with the only method', function () { $testSuite = new TestSuite(getcwd(), 'tests'); - $test = function () {}; - $testWithOnly = new TestCaseFactory(__FILE__, 'foo', $test); + $testWithOnly = new TestCaseMethodFactory('a', 'b', null); $testWithOnly->only = true; $testSuite->tests->set($testWithOnly); - $testSuite->tests->set(new TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test)); + $testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null)); expect($testSuite->tests->getFilenames())->toEqual([ - __FILE__, + 'a', ]); }); @@ -59,15 +61,15 @@ it('does not filter the test suite filenames to those with the only method when $test = function () {}; - $testWithOnly = new TestCaseFactory(__FILE__, 'foo', $test); + $testWithOnly = new TestCaseMethodFactory('a', 'b', null); $testWithOnly->only = true; $testSuite->tests->set($testWithOnly); - $testSuite->tests->set(new TestCaseFactory('Baz/Bar/Boo.php', 'bar', $test)); + $testSuite->tests->set(new TestCaseMethodFactory('c', 'd', null)); expect($testSuite->tests->getFilenames())->toEqual([ - __FILE__, - 'Baz/Bar/Boo.php', + 'a', + 'c', ]); Environment::name($previousEnvironment); From 183f97516678c528a5af936623df1c97435f5f49 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 14 Nov 2021 21:23:02 +0000 Subject: [PATCH 15/31] chore: phpstan level 5 --- .github/workflows/tests.yml | 5 + composer.json | 10 +- phpstan.neon | 13 +- src/Bootstrappers/BootSubscribers.php | 1 - src/Datasets.php | 4 +- src/Factories/TestCaseMethodFactory.php | 9 +- src/Functions.php | 24 +- src/Kernel.php | 2 +- src/Logging/JUnit.php | 373 +------------------ src/Logging/TeamCity.php | 282 +------------- src/OppositeExpectation.php | 12 +- src/PendingCalls/TestCall.php | 2 +- src/Plugins/Actions/AddsOutput.php | 4 +- src/Plugins/Actions/HandleArguments.php | 2 + src/Repositories/AfterEachRepository.php | 1 - src/Repositories/TestRepository.php | 4 +- src/Subscribers/EnsureTestsAreLoaded.php | 82 ---- src/Support/ChainableClosure.php | 10 +- src/Support/Container.php | 1 - src/Support/HigherOrderCallables.php | 2 +- src/Support/HigherOrderMessage.php | 7 +- src/Support/HigherOrderMessageCollection.php | 4 +- src/Support/HigherOrderTapProxy.php | 7 +- 23 files changed, 65 insertions(+), 796 deletions(-) delete mode 100644 src/Subscribers/EnsureTestsAreLoaded.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 41654677..fce3d953 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,8 +38,13 @@ jobs: - name: Install PHP dependencies run: composer update --${{ matrix.dependency-version }} --no-interaction --no-progress + - name: Unit Tests + run: php bin/pest --colors=always --exclude-group=integration + - name: Unit Tests run: php bin/pest --colors=always --exclude-group=integration ${{ matrix.parallel }} + if: ${{ false }} # 2.x-dev is under development - name: Integration Tests run: php bin/pest --colors=always --group=integration + if: ${{ false }} # 2.x-dev is under development diff --git a/composer.json b/composer.json index 1b8e7aaa..c8a602ee 100644 --- a/composer.json +++ b/composer.json @@ -64,15 +64,13 @@ "test:lint": "php-cs-fixer fix -v --dry-run", "test:types": "phpstan analyse --ansi --memory-limit=-1", "test:unit": "php bin/pest --colors=always --exclude-group=integration", - "test:parallel": "php bin/pest -p --colors=always --exclude-group=integration", - "test:integration": "php bin/pest --colors=always --group=integration", - "update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always", + "test:parallel": "exit 1", + "test:integration": "exit 1", + "update:snapshots": "exit 1", "test": [ "@test:lint", "@test:types", - "@test:unit", - "@test:parallel", - "@test:integration" + "@test:unit" ] }, "extra": { diff --git a/phpstan.neon b/phpstan.neon index a32cf76a..d5dc4ef6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,6 +13,7 @@ parameters: reportUnmatchedIgnoredErrors: true ignoreErrors: + - "#with a nullable type declaration#" - "#type mixed is not subtype of native#" - "#is not allowed to extend#" - "#Language construct eval#" @@ -20,15 +21,3 @@ parameters: - "#has parameter \\$closure with default value.#" - "#has parameter \\$description with default value.#" - "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#" - - - message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#' - path: src/Logging - - - message: '#invalid typehint type Pest\\Concerns\\Testable#' - path: src/Logging - - - message: '#is not subtype of native type PHPUnit\\Framework\\Test#' - path: src/Logging - - - message: '#Call to an undefined method PHPUnit\\Framework\\Test::getPrintableTestCaseName\(\)#' - path: src/Logging diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index a3ef3dc5..c23e9bf0 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -18,7 +18,6 @@ final class BootSubscribers * @var array */ private static array $subscribers = [ - Subscribers\EnsureTestsAreLoaded::class, Subscribers\EnsureConfigurationIsValid::class, Subscribers\EnsureConfigurationDefaults::class, ]; diff --git a/src/Datasets.php b/src/Datasets.php index 38cc6662..0cf59d60 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -46,7 +46,7 @@ final class Datasets /** * Sets the given. * - * @param Closure|iterable $data + * @param Closure|iterable|string $with */ public static function with(string $filename, string $description, Closure|iterable|string $with): void { @@ -129,7 +129,7 @@ final class Datasets $processedDataset = []; if (is_string($data)) { - if (!isset(self::$datasets[$data])) { + if (! array_key_exists($data, self::$datasets)) { throw new DatasetDoesNotExist($data); } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index dd32fceb..1c16d5f6 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -17,7 +17,6 @@ use PHPUnit\Framework\TestCase; final class TestCaseMethodFactory { use HigherOrderable; - /** * Determines if the Test Case will be the "only" being run. */ @@ -54,7 +53,7 @@ final class TestCaseMethodFactory ) { if ($this->closure === null) { $this->closure = function () { - Assert::getCount() > 0 ?: self::markTestIncomplete(); + Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line }; } @@ -66,7 +65,7 @@ final class TestCaseMethodFactory */ public function getClosure(TestCase $concrete): Closure { - $concrete::flush(); + $concrete::flush(); // @phpstan-ignore-line if ($this->description === null) { throw ShouldNotHappen::fromMessage('Description can not be empty.'); @@ -81,7 +80,9 @@ final class TestCaseMethodFactory $method = $this; - return function () use ($testCase, $method, $closure): mixed { + return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line + /** @var TestCase $this */ + $testCase->proxies->proxy($this); $method->proxies->proxy($this); diff --git a/src/Functions.php b/src/Functions.php index 20a0ee6e..1e306999 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -14,18 +14,20 @@ use Pest\Support\HigherOrderTapProxy; use Pest\TestSuite; use PHPUnit\Framework\TestCase; -/** - * Creates a new expectation. - * - * @param mixed $value the Value - */ -function expect($value = null): Expectation|Extendable -{ - if (func_num_args() === 0) { - return new Extendable(Expectation::class); - } +if (!function_exists('expect')) { + /** + * Creates a new expectation. + * + * @param mixed $value the Value + */ + function expect($value = null): Expectation|Extendable + { + if (func_num_args() === 0) { + return new Extendable(Expectation::class); + } - return new Expectation($value); + return new Expectation($value); + } } if (!function_exists('beforeAll')) { diff --git a/src/Kernel.php b/src/Kernel.php index ff4eba3a..af2d82e6 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -64,6 +64,6 @@ final class Kernel */ public function shutdown(): void { - // TODO + // .. } } diff --git a/src/Logging/JUnit.php b/src/Logging/JUnit.php index 14595bbc..91921b31 100644 --- a/src/Logging/JUnit.php +++ b/src/Logging/JUnit.php @@ -39,376 +39,7 @@ use function trim; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ -final class JUnit extends Printer implements TestListener +final class JUnit extends Printer { - private DOMDocument $document; - - private DOMElement $root; - - /** - * @var array - */ - private array $testSuites = []; - - /** - * @var int[] - */ - private array $testSuiteTests = [0]; - - /** - * @var int[] - */ - private array $testSuiteAssertions = [0]; - - /** - * @var int[] - */ - private array $testSuiteErrors = [0]; - - /** - * @var int[] - */ - private array $testSuiteWarnings = [0]; - - /** - * @var int[] - */ - private array $testSuiteFailures = [0]; - - /** - * @var int[] - */ - private array $testSuiteSkipped = [0]; - - private array $testSuiteTimes = [0]; - - private int $testSuiteLevel = 0; - - private ?DOMElement $currentTestCase = null; - - public function __construct(string $out) - { - $this->document = new DOMDocument('1.0', 'UTF-8'); - $this->document->formatOutput = true; - - $this->root = $this->document->createElement('testsuites'); - $this->document->appendChild($this->root); - - parent::__construct($out); - } - - /** - * Flush buffer and close output. - */ - public function flush(): void - { - $this->write($this->getXML()); - - parent::flush(); - } - - /** - * An error occurred. - */ - public function addError(Test $test, Throwable $t, float $time): void - { - $this->doAddFault($test, $t, 'error'); - $this->testSuiteErrors[$this->testSuiteLevel]++; - } - - /** - * A warning occurred. - */ - public function addWarning(Test $test, Warning $e, float $time): void - { - $this->doAddFault($test, $e, 'warning'); - $this->testSuiteWarnings[$this->testSuiteLevel]++; - } - - /** - * A failure occurred. - */ - public function addFailure(Test $test, AssertionFailedError $e, float $time): void - { - $this->doAddFault($test, $e, 'failure'); - $this->testSuiteFailures[$this->testSuiteLevel]++; - } - - /** - * Incomplete test. - */ - public function addIncompleteTest(Test $test, Throwable $t, float $time): void - { - $this->doAddSkipped(); - } - - /** - * Risky test. - */ - public function addRiskyTest(Test $test, Throwable $t, float $time): void - { - } - - /** - * Skipped test. - */ - public function addSkippedTest(Test $test, Throwable $t, float $time): void - { - $this->doAddSkipped(); - } - - /** @phpstan-ignore-next-line */ - public function startTestSuite(TestSuite $suite): void - { - $testSuite = $this->document->createElement('testsuite'); - $testSuite->setAttribute('name', $suite->getName()); - - if (class_exists($suite->getName(), false)) { - try { - $class = new ReflectionClass($suite->getName()); - - if ($class->hasMethod('__getFileName')) { - $fileName = $class->getMethod('__getFileName')->invoke(null); - } else { - $fileName = $class->getFileName(); - } - - $testSuite->setAttribute('file', $fileName); - } catch (ReflectionException) { - // @ignoreException - } - } - - if ($this->testSuiteLevel > 0) { - $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); - } else { - $this->root->appendChild($testSuite); - } - - $this->testSuiteLevel++; - $this->testSuites[$this->testSuiteLevel] = $testSuite; - $this->testSuiteTests[$this->testSuiteLevel] = 0; - $this->testSuiteAssertions[$this->testSuiteLevel] = 0; - $this->testSuiteErrors[$this->testSuiteLevel] = 0; - $this->testSuiteWarnings[$this->testSuiteLevel] = 0; - $this->testSuiteFailures[$this->testSuiteLevel] = 0; - $this->testSuiteSkipped[$this->testSuiteLevel] = 0; - $this->testSuiteTimes[$this->testSuiteLevel] = 0; - } - - /** @phpstan-ignore-next-line */ - public function endTestSuite(TestSuite $suite): void - { - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'tests', - (string) $this->testSuiteTests[$this->testSuiteLevel] - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'assertions', - (string) $this->testSuiteAssertions[$this->testSuiteLevel] - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'errors', - (string) $this->testSuiteErrors[$this->testSuiteLevel] - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'warnings', - (string) $this->testSuiteWarnings[$this->testSuiteLevel] - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'failures', - (string) $this->testSuiteFailures[$this->testSuiteLevel] - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'skipped', - (string) $this->testSuiteSkipped[$this->testSuiteLevel] - ); - - $this->testSuites[$this->testSuiteLevel]->setAttribute( - 'time', - sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]) - ); - - if ($this->testSuiteLevel > 1) { - $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; - $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; - $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; - $this->testSuiteWarnings[$this->testSuiteLevel - 1] += $this->testSuiteWarnings[$this->testSuiteLevel]; - $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; - $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel]; - $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; - } - - $this->testSuiteLevel--; - } - - /** - * A test started. - * - * @param Test|Testable $test - */ - public function startTest(Test $test): void - { - $usesDataprovider = false; - - if (method_exists($test, 'usesDataProvider')) { - $usesDataprovider = $test->usesDataProvider(); - } - - $testCase = $this->document->createElement('testcase'); - $testCase->setAttribute('name', $test->getName()); - - try { - $class = new ReflectionClass($test); - // @codeCoverageIgnoreStart - } catch (ReflectionException $e) { - // @phpstan-ignore-next-line - throw new Exception($e->getMessage(), (int) $e->getCode(), $e); - } - // @codeCoverageIgnoreEnd - - $methodName = $test->getName(!$usesDataprovider); - - if ($class->hasMethod($methodName)) { - try { - $method = $class->getMethod($methodName); - // @codeCoverageIgnoreStart - } catch (ReflectionException $e) { - // @phpstan-ignore-next-line - throw new Exception($e->getMessage(), (int) $e->getCode(), $e); - } - // @codeCoverageIgnoreEnd - - $testCase->setAttribute('class', $class->getName()); - $testCase->setAttribute('classname', str_replace('\\', '.', $class->getName())); - $fileName = $class->getFileName(); - if ($fileName !== false) { - $testCase->setAttribute('file', $fileName); - } - $testCase->setAttribute('line', (string) $method->getStartLine()); - } - - if (TeamCity::isPestTest($test)) { - $testCase->setAttribute('class', $test->getPrintableTestCaseName()); - $testCase->setAttribute('classname', str_replace('\\', '.', $test->getPrintableTestCaseName())); - // @phpstan-ignore-next-line - $testCase->setAttribute('file', $test->__getFilename()); - } - - $this->currentTestCase = $testCase; - } - - /** - * A test ended. - */ - public function endTest(Test $test, float $time): void - { - $numAssertions = 0; - - if (method_exists($test, 'getNumAssertions')) { - $numAssertions = $test->getNumAssertions(); - } - - $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; - - if ($this->currentTestCase !== null) { - $this->currentTestCase->setAttribute( - 'assertions', - (string) $numAssertions - ); - - $this->currentTestCase->setAttribute( - 'time', - sprintf('%F', $time) - ); - - $this->testSuites[$this->testSuiteLevel]->appendChild( - $this->currentTestCase - ); - } - - $this->testSuiteTests[$this->testSuiteLevel]++; - $this->testSuiteTimes[$this->testSuiteLevel] += $time; - - $testOutput = ''; - - if (method_exists($test, 'hasOutput') && method_exists($test, 'getActualOutput')) { - $testOutput = $test->hasOutput() ? $test->getActualOutput() : ''; - } - - if ($testOutput !== '') { - $systemOut = $this->document->createElement( - 'system-out', - Xml::prepareString($testOutput) - ); - - if ($this->currentTestCase !== null) { - $this->currentTestCase->appendChild($systemOut); - } - } - - $this->currentTestCase = null; - } - - /** - * Returns the XML as a string. - */ - public function getXML(): string - { - $xml = $this->document->saveXML(); - if ($xml === false) { - return ''; - } - - return $xml; - } - - private function doAddFault(Test $test, Throwable $t, string $type): void - { - if ($this->currentTestCase === null) { - return; - } - - if ($test instanceof SelfDescribing) { - $buffer = $test->toString() . "\n"; - } else { - $buffer = ''; - } - - $buffer .= trim( - TestFailure::exceptionToString($t) . "\n" . - Filter::getFilteredStacktrace($t) - ); - - $fault = $this->document->createElement( - $type, - Xml::prepareString($buffer) - ); - - if ($t instanceof ExceptionWrapper) { - $fault->setAttribute('type', $t->getClassName()); - } else { - $fault->setAttribute('type', $t::class); - } - - $this->currentTestCase->appendChild($fault); - } - - private function doAddSkipped(): void - { - if ($this->currentTestCase === null) { - return; - } - - $skipped = $this->document->createElement('skipped'); - - $this->currentTestCase->appendChild($skipped); - - $this->testSuiteSkipped[$this->testSuiteLevel]++; - } + // @todo } diff --git a/src/Logging/TeamCity.php b/src/Logging/TeamCity.php index 76093f38..005cbd24 100644 --- a/src/Logging/TeamCity.php +++ b/src/Logging/TeamCity.php @@ -4,289 +4,9 @@ declare(strict_types=1); namespace Pest\Logging; -use function getmypid; -use Pest\Concerns\Logging\WritesToConsole; -use Pest\Concerns\Testable; -use Pest\Support\ExceptionTrace; -use function Pest\version; -use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\TestResult; -use PHPUnit\Framework\TestSuite; -use PHPUnit\Framework\Warning; use PHPUnit\TextUI\DefaultResultPrinter; -use PHPUnit\TextUI\XmlConfiguration\Logging\TeamCity as BaseTeamCity; -use function round; -use function str_replace; -use Throwable; final class TeamCity extends DefaultResultPrinter { - use WritesToConsole; - private const PROTOCOL = 'pest_qn://'; - private const NAME = 'name'; - private const LOCATION_HINT = 'locationHint'; - private const DURATION = 'duration'; - private const TEST_SUITE_STARTED = 'testSuiteStarted'; - private const TEST_SUITE_FINISHED = 'testSuiteFinished'; - private const TEST_COUNT = 'testCount'; - private const TEST_STARTED = 'testStarted'; - private const TEST_FINISHED = 'testFinished'; - - private ?int $flowId = null; - - private bool $isSummaryTestCountPrinted = false; - - private BaseTeamCity $phpunitTeamCity; - - /** - * Creates a new printer instance. - */ - public function __construct(string|null $out, bool $verbose, string $colors) - { - parent::__construct($out, $verbose, $colors); - $this->phpunitTeamCity = new BaseTeamCity($out, $verbose, $colors); - - $this->logo(); - } - - private function logo(): void - { - $this->writeNewLine(); - $this->write('Pest ' . version()); - $this->writeNewLine(); - } - - public function printResult(TestResult $result): void - { - $this->write('Tests: '); - - $results = [ - 'failed' => ['count' => $result->errorCount() + $result->failureCount(), 'color' => 'fg-red'], - 'skipped' => ['count' => $result->skippedCount(), 'color' => 'fg-yellow'], - 'warned' => ['count' => $result->warningCount(), 'color' => 'fg-yellow'], - 'risked' => ['count' => $result->riskyCount(), 'color' => 'fg-yellow'], - 'incomplete' => ['count' => $result->notImplementedCount(), 'color' => 'fg-yellow'], - 'passed' => ['count' => $this->successfulTestCount($result), 'color' => 'fg-green'], - ]; - - $filteredResults = array_filter($results, fn ($item): bool => $item['count'] > 0); - - foreach ($filteredResults as $key => $info) { - $this->writeWithColor($info['color'], $info['count'] . " $key", false); - - if ($key !== array_reverse(array_keys($filteredResults))[0]) { - $this->write(', '); - } - } - - $this->writeNewLine(); - $this->write("Assertions: $this->numAssertions"); - - $this->writeNewLine(); - $this->write("Time: {$result->time()}s"); - - $this->writeNewLine(); - } - - private function successfulTestCount(TestResult $result): int - { - return $result->count() - - $result->failureCount() - - $result->errorCount() - - $result->skippedCount() - - $result->warningCount() - - $result->notImplementedCount() - - $result->riskyCount(); - } - - /** @phpstan-ignore-next-line */ - public function startTestSuite(TestSuite $suite): void - { - $suiteName = $suite->getName(); - - if (static::isCompoundTestSuite($suite)) { - $this->writeWithColor('bold', ' ' . $suiteName); - } elseif (static::isPestTestSuite($suite)) { - $this->writeWithColor('fg-white, bold', ' ' . substr_replace($suiteName, '', 0, 2) . ' '); - } else { - $this->writeWithColor('fg-white, bold', ' ' . $suiteName); - } - - $this->writeNewLine(); - - $this->flowId = (int) getmypid(); - - if (!$this->isSummaryTestCountPrinted) { - $this->printEvent(self::TEST_COUNT, [ - 'count' => $suite->count(), - ]); - $this->isSummaryTestCountPrinted = true; - } - - $this->printEvent(self::TEST_SUITE_STARTED, [ - self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2), - self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()), - ]); - } - - /** - * @param array $params - */ - private function printEvent(string $eventName, array $params = []): void - { - $this->write("##teamcity[{$eventName}"); - - if ($this->flowId !== 0) { - $params['flowId'] = $this->flowId; - } - - foreach ($params as $key => $value) { - $escapedValue = self::escapeValue((string) $value); - $this->write(" {$key}='{$escapedValue}'"); - } - - $this->write("]\n"); - } - - private static function escapeValue(string $text): string - { - return str_replace( - ['|', "'", "\n", "\r", ']', '['], - ['||', "|'", '|n', '|r', '|]', '|['], - $text - ); - } - - /** @phpstan-ignore-next-line */ - public function endTestSuite(TestSuite $suite): void - { - $suiteName = $suite->getName(); - - $this->writeNewLine(); - $this->writeNewLine(); - - $this->printEvent(self::TEST_SUITE_FINISHED, [ - self::NAME => static::isCompoundTestSuite($suite) ? $suiteName : substr($suiteName, 2), - self::LOCATION_HINT => self::PROTOCOL . (static::isCompoundTestSuite($suite) ? $suiteName : $suiteName::__getFileName()), - ]); - } - - /** - * @param Test|Testable $test - */ - public function startTest(Test $test): void - { - if (!TeamCity::isPestTest($test)) { - $this->phpunitTeamCity->startTest($test); - - return; - } - - $this->printEvent(self::TEST_STARTED, [ - self::NAME => $test->getName(), - // @phpstan-ignore-next-line - self::LOCATION_HINT => self::PROTOCOL . $test->toString(), - ]); - } - - /** - * Verify that the given test suite is a valid Pest suite. - * - * @param TestSuite $suite - */ - private static function isPestTestSuite(TestSuite $suite): bool - { - return str_starts_with($suite->getName(), 'P\\'); - } - - /** - * Determine if the test suite is made up of multiple smaller test suites. - * - * @param TestSuite $suite - */ - private static function isCompoundTestSuite(TestSuite $suite): bool - { - return file_exists($suite->getName()) || !method_exists($suite->getName(), '__getFileName'); - } - - public static function isPestTest(Test $test): bool - { - /** @var array $uses */ - $uses = class_uses($test); - - return in_array(Testable::class, $uses, true); - } - - /** - * @param Test|Testable $test - */ - public function endTest(Test $test, float $time): void - { - $this->printEvent(self::TEST_FINISHED, [ - self::NAME => $test->getName(), - self::DURATION => self::toMilliseconds($time), - ]); - - if (!$this->lastTestFailed) { - $this->writeSuccess($test->getName()); - } - - $this->numAssertions += $test instanceof TestCase ? $test->getNumAssertions() : 1; - $this->lastTestFailed = false; - } - - private static function toMilliseconds(float $time): int - { - return (int) round($time * 1000); - } - - public function addError(Test $test, Throwable $t, float $time): void - { - $this->markAsFailure($t); - $this->writeError($test->getName()); - $this->phpunitTeamCity->addError($test, $t, $time); - } - - public function addFailure(Test $test, AssertionFailedError $e, float $time): void - { - $this->markAsFailure($e); - $this->writeError($test->getName()); - $this->phpunitTeamCity->addFailure($test, $e, $time); - } - - public function addWarning(Test $test, Warning $e, float $time): void - { - $this->markAsFailure($e); - $this->writeWarning($test->getName()); - $this->phpunitTeamCity->addWarning($test, $e, $time); - } - - public function addIncompleteTest(Test $test, Throwable $t, float $time): void - { - $this->markAsFailure($t); - $this->writeWarning($test->getName()); - $this->phpunitTeamCity->addIncompleteTest($test, $t, $time); - } - - public function addRiskyTest(Test $test, Throwable $t, float $time): void - { - $this->markAsFailure($t); - $this->writeWarning($test->getName()); - $this->phpunitTeamCity->addRiskyTest($test, $t, $time); - } - - public function addSkippedTest(Test $test, Throwable $t, float $time): void - { - $this->markAsFailure($t); - $this->writeWarning($test->getName()); - $this->phpunitTeamCity->printIgnoredTest($test->getName(), $t, $time); - } - - private function markAsFailure(Throwable $t): void - { - $this->lastTestFailed = true; - ExceptionTrace::removePestReferences($t); - } + // @todo } diff --git a/src/OppositeExpectation.php b/src/OppositeExpectation.php index 7a17162c..e56eabd3 100644 --- a/src/OppositeExpectation.php +++ b/src/OppositeExpectation.php @@ -46,6 +46,8 @@ final class OppositeExpectation * Handle dynamic method calls into the original expectation. * * @param array $arguments + * + * @return Expectation|never */ public function __call(string $name, array $arguments): Expectation { @@ -56,23 +58,23 @@ final class OppositeExpectation return $this->original; } - // @phpstan-ignore-next-line $this->throwExpectationFailedException($name, $arguments); } /** * Handle dynamic properties gets into the original expectation. + * + * @return Expectation|never */ public function __get(string $name): Expectation { try { - /* @phpstan-ignore-next-line */ - $this->original->{$name}; + /** @throws ExpectationFailedException */ + $this->original->{$name}; // @phpstan-ignore-line } catch (ExpectationFailedException) { return $this->original; } - // @phpstan-ignore-next-line $this->throwExpectationFailedException($name); } @@ -80,6 +82,8 @@ final class OppositeExpectation * Creates a new expectation failed exception with a nice readable message. * * @param array $arguments + * + * @return never */ private function throwExpectationFailedException(string $name, array $arguments = []): void { diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index 25c6426e..0d1cd17c 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -74,7 +74,7 @@ final class TestCall $condition = is_callable($condition) ? $condition : static function () use ($condition): bool { - return $condition; // @phpstan-ignore-line + return $condition; }; if ($condition()) { diff --git a/src/Plugins/Actions/AddsOutput.php b/src/Plugins/Actions/AddsOutput.php index 967ff0a2..fefac940 100644 --- a/src/Plugins/Actions/AddsOutput.php +++ b/src/Plugins/Actions/AddsOutput.php @@ -1,5 +1,7 @@ addOutput($exitCode); } diff --git a/src/Plugins/Actions/HandleArguments.php b/src/Plugins/Actions/HandleArguments.php index f0369d01..e26a9baa 100644 --- a/src/Plugins/Actions/HandleArguments.php +++ b/src/Plugins/Actions/HandleArguments.php @@ -1,5 +1,7 @@ addToAssertionCount($container->mockery_getExpectationCount()); diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 57d8e12c..5ab98700 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -90,7 +90,7 @@ final class TestRepository */ public function set(TestCaseMethodFactory $method): void { - if (!isset($this->testCases[$method->filename])) { + if (! array_key_exists($method->filename, $this->testCases)) { $this->testCases[$method->filename] = new TestCaseFactory($method->filename); } @@ -102,7 +102,7 @@ final class TestRepository */ public function makeIfExists(string $filename): void { - if (isset($this->testCases[$filename])) { + if (array_key_exists($filename, $this->testCases)) { $this->make($this->testCases[$filename]); } } diff --git a/src/Subscribers/EnsureTestsAreLoaded.php b/src/Subscribers/EnsureTestsAreLoaded.php deleted file mode 100644 index 23954c24..00000000 --- a/src/Subscribers/EnsureTestsAreLoaded.php +++ /dev/null @@ -1,82 +0,0 @@ -removeWarnings(self::$testSuite); - - $testSuites = []; - - $testSuite = \Pest\TestSuite::getInstance(); - $testSuite->tests->build($testSuite, function (TestCase $testCase) use (&$testSuites): void { - $testCaseClass = $testCase::class; - if (!array_key_exists($testCaseClass, $testSuites)) { - $testSuites[$testCaseClass] = []; - } - - $testSuites[$testCaseClass][] = $testCase; - }); - - foreach ($testSuites as $testCaseName => $testCases) { - $testTestSuite = new TestSuite($testCaseName); - $testTestSuite->setTests([]); - foreach ($testCases as $testCase) { - $testTestSuite->addTest($testCase, $testCase->groups()); - } - self::$testSuite->addTestSuite($testTestSuite); - } - */ - } - - /** - * Sets the current test suite. - */ - public static function setTestSuite(TestSuite $testSuite): void - { - self::$testSuite = $testSuite; - } - - /** - * Removes the test case that have "empty" warnings. - */ - private function removeWarnings(TestSuite $testSuite): void - { - $tests = $testSuite->tests(); - - foreach ($tests as $key => $test) { - if ($test instanceof TestSuite) { - $this->removeWarnings($test); - } - - if ($test instanceof WarningTestCase) { - unset($tests[$key]); - } - } - - $testSuite->setTests(array_values($tests)); - } -} diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index 5ce0f756..b37e4f5f 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace Pest\Support; use Closure; +use Pest\Exceptions\ShouldNotHappen; +use PHPUnit\Framework\TestCase; /** * @internal @@ -17,9 +19,11 @@ final class ChainableClosure public static function from(Closure $closure, Closure $next): Closure { return function () use ($closure, $next): void { - /* @phpstan-ignore-next-line */ + if (! is_object($this)) { // @phpstan-ignore-line + throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); + } + call_user_func_array(Closure::bind($closure, $this, $this::class), func_get_args()); - /* @phpstan-ignore-next-line */ call_user_func_array(Closure::bind($next, $this, $this::class), func_get_args()); }; } @@ -30,9 +34,7 @@ final class ChainableClosure public static function fromStatic(Closure $closure, Closure $next): Closure { return static function () use ($closure, $next): void { - /* @phpstan-ignore-next-line */ call_user_func_array(Closure::bind($closure, null, self::class), func_get_args()); - /* @phpstan-ignore-next-line */ call_user_func_array(Closure::bind($next, null, self::class), func_get_args()); }; } diff --git a/src/Support/Container.php b/src/Support/Container.php index 6be1a4e1..6390484b 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -63,7 +63,6 @@ final class Container */ private function build(string $id): object { - /** @phpstan-ignore-next-line */ $reflectionClass = new ReflectionClass($id); if ($reflectionClass->isInstantiable()) { diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php index ea032a88..1637ccb2 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 $value + * @param (callable():TValue)|TValue $value * * @return Expectation */ diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index 1df45e4e..f756c64d 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -18,14 +18,14 @@ final class HigherOrderMessage /** * An optional condition that will determine if the message will be executed. * - * @var (callable(): bool)|null + * @var (Closure(): bool)|null */ - public $condition; + public ?Closure $condition = null; /** * Creates a new higher order message. * - * @param array|null $arguments + * @param array $arguments */ public function __construct( public string $filename, @@ -41,7 +41,6 @@ final class HigherOrderMessage */ public function call(object $target): mixed { - /* @phpstan-ignore-next-line */ if (is_callable($this->condition) && call_user_func(Closure::bind($this->condition, $target)) === false) { return $target; } diff --git a/src/Support/HigherOrderMessageCollection.php b/src/Support/HigherOrderMessageCollection.php index b0adf7b4..cb69a3a9 100644 --- a/src/Support/HigherOrderMessageCollection.php +++ b/src/Support/HigherOrderMessageCollection.php @@ -19,7 +19,7 @@ final class HigherOrderMessageCollection * * @param array|null $arguments */ - public function add(string $filename, int $line, string $name, array $arguments = null): void + public function add(string $filename, int $line, string $name, ?array $arguments): void { $this->messages[] = new HigherOrderMessage($filename, $line, $name, $arguments); } @@ -29,7 +29,7 @@ final class HigherOrderMessageCollection * * @param array|null $arguments */ - public function addWhen(callable $condition, string $filename, int $line, string $name, array $arguments = null): void + public function addWhen(callable $condition, string $filename, int $line, string $name, ?array $arguments): void { $this->messages[] = (new HigherOrderMessage($filename, $line, $name, $arguments))->when($condition); } diff --git a/src/Support/HigherOrderTapProxy.php b/src/Support/HigherOrderTapProxy.php index f76026ae..bfce21c8 100644 --- a/src/Support/HigherOrderTapProxy.php +++ b/src/Support/HigherOrderTapProxy.php @@ -31,8 +31,7 @@ final class HigherOrderTapProxy */ public function __set(string $property, $value): void { - // @phpstan-ignore-next-line - $this->target->{$property} = $value; + $this->target->{$property} = $value; // @phpstan-ignore-line } /** @@ -43,8 +42,8 @@ final class HigherOrderTapProxy public function __get(string $property) { try { - // @phpstan-ignore-next-line - return $this->target->{$property}; + /** @throws Throwable */ + return $this->target->{$property}; // @phpstan-ignore-line } catch (Throwable $throwable) { Reflection::setPropertyValue($throwable, 'file', Backtrace::file()); Reflection::setPropertyValue($throwable, 'line', Backtrace::line()); From b2cd60395f8c0c75335ed769975b499e7f83beb7 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 14 Nov 2021 21:39:24 +0000 Subject: [PATCH 16/31] chore: phpstan level 5 --- composer.json | 2 +- phpstan.neon | 2 +- src/Datasets.php | 2 +- src/Expectation.php | 16 +++++++--------- src/Factories/TestCaseFactory.php | 14 ++++++++------ src/Factories/TestCaseMethodFactory.php | 2 +- src/Logging/JUnit.php | 22 ---------------------- src/OppositeExpectation.php | 3 +-- src/Repositories/TestRepository.php | 2 +- src/Support/ChainableClosure.php | 3 +-- src/Support/HigherOrderTapProxy.php | 5 ++--- 11 files changed, 24 insertions(+), 49 deletions(-) diff --git a/composer.json b/composer.json index c8a602ee..8b4bdf03 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "scripts": { "lint": "php-cs-fixer fix -v", "test:lint": "php-cs-fixer fix -v --dry-run", - "test:types": "phpstan analyse --ansi --memory-limit=-1", + "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:unit": "php bin/pest --colors=always --exclude-group=integration", "test:parallel": "exit 1", "test:integration": "exit 1", diff --git a/phpstan.neon b/phpstan.neon index d5dc4ef6..c1a0e56e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: max + level: 5 paths: - src diff --git a/src/Datasets.php b/src/Datasets.php index 0cf59d60..429de157 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -129,7 +129,7 @@ final class Datasets $processedDataset = []; if (is_string($data)) { - if (! array_key_exists($data, self::$datasets)) { + if (!array_key_exists($data, self::$datasets)) { throw new DatasetDoesNotExist($data); } diff --git a/src/Expectation.php b/src/Expectation.php index 28ea26af..920a5f70 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -74,11 +74,9 @@ final class Expectation /** * Dump the expectation value and end the script. * - * @param mixed $arguments - * * @return never */ - public function dd(...$arguments): void + public function dd(mixed ...$arguments): void { if (function_exists('dd')) { dd($this->value, ...$arguments); @@ -91,13 +89,10 @@ final class Expectation /** * Send the expectation value to Ray along with all given arguments. - * - * @param ...mixed $arguments */ public function ray(mixed ...$arguments): self { if (function_exists('ray')) { - // @phpstan-ignore-next-line ray($this->value, ...$arguments); } @@ -224,7 +219,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); @@ -241,7 +236,7 @@ final class Expectation $condition = is_callable($condition) ? $condition : static function () use ($condition): bool { - return $condition; // @phpstan-ignore-line + return $condition; }; if ($condition()) { @@ -371,6 +366,8 @@ final class Expectation /** * Asserts that the value starts with $expected. + * + * @param non-empty-string $expected */ public function toStartWith(string $expected): Expectation { @@ -381,6 +378,8 @@ final class Expectation /** * Asserts that the value ends with $expected. + * + * @param non-empty-string $expected */ public function toEndWith(string $expected): Expectation { @@ -526,7 +525,6 @@ final class Expectation */ public function toBeInstanceOf(string $class): Expectation { - /* @phpstan-ignore-next-line */ Assert::assertInstanceOf($class, $this->value); return $this; diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 8882a0da..d21b5513 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -71,11 +71,13 @@ final class TestCaseFactory public function make(): void { - $methods = array_filter($this->methods, function ($method) { - return count($onlyTestCases = $this->methodsUsingOnly()) === 0 || in_array($method, $onlyTestCases, true); + $methodsUsingOnly = $this->methodsUsingOnly(); + + $methods = array_filter($this->methods, function ($method) use ($methodsUsingOnly) { + return count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true); }); - if (count($this->methods) > 0) { + if (count($methods) > 0) { $this->evaluate($this->filename, $methods); } } @@ -91,7 +93,7 @@ final class TestCaseFactory return []; } - return array_filter($this->methods, static fn ($method): bool => $method->only); + return array_values(array_filter($this->methods, static fn ($method): bool => $method->only)); } /** @@ -147,7 +149,7 @@ final class TestCaseFactory $annotations = (new $annotation())->add($method, $annotations); } - if (!empty($method->datasets)) { + if (count($method->datasets) > 0) { $dataProviderName = $methodName . '_dataset'; $annotations[] = "@dataProvider $dataProviderName"; @@ -214,7 +216,7 @@ EOF; throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } - if (isset($this->methods[$method->description])) { + if (array_key_exists($method->description, $this->methods)) { throw new TestAlreadyExist($method->filename, $method->description); } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 1c16d5f6..ffa4cb4b 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -81,7 +81,7 @@ final class TestCaseMethodFactory $method = $this; return function () use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line - /** @var TestCase $this */ + /* @var TestCase $this */ $testCase->proxies->proxy($this); $method->proxies->proxy($this); diff --git a/src/Logging/JUnit.php b/src/Logging/JUnit.php index 91921b31..207912ac 100644 --- a/src/Logging/JUnit.php +++ b/src/Logging/JUnit.php @@ -12,29 +12,7 @@ declare(strict_types=1); namespace Pest\Logging; -use function class_exists; -use DOMDocument; -use DOMElement; -use Exception; -use function method_exists; -use Pest\Concerns\Testable; -use PHPUnit\Framework\AssertionFailedError; -use PHPUnit\Framework\ExceptionWrapper; -use PHPUnit\Framework\SelfDescribing; -use PHPUnit\Framework\Test; -use PHPUnit\Framework\TestFailure; -use PHPUnit\Framework\TestListener; -use PHPUnit\Framework\TestSuite; -use PHPUnit\Framework\Warning; -use PHPUnit\Util\Filter; use PHPUnit\Util\Printer; -use PHPUnit\Util\Xml; -use ReflectionClass; -use ReflectionException; -use function sprintf; -use function str_replace; -use Throwable; -use function trim; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit diff --git a/src/OppositeExpectation.php b/src/OppositeExpectation.php index e56eabd3..e4ec3d85 100644 --- a/src/OppositeExpectation.php +++ b/src/OppositeExpectation.php @@ -69,9 +69,8 @@ final class OppositeExpectation public function __get(string $name): Expectation { try { - /** @throws ExpectationFailedException */ $this->original->{$name}; // @phpstan-ignore-line - } catch (ExpectationFailedException) { + } catch (ExpectationFailedException) { // @phpstan-ignore-line return $this->original; } diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 5ab98700..b8e2b53e 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -90,7 +90,7 @@ final class TestRepository */ public function set(TestCaseMethodFactory $method): void { - if (! array_key_exists($method->filename, $this->testCases)) { + if (!array_key_exists($method->filename, $this->testCases)) { $this->testCases[$method->filename] = new TestCaseFactory($method->filename); } diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index b37e4f5f..8a136682 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -6,7 +6,6 @@ namespace Pest\Support; use Closure; use Pest\Exceptions\ShouldNotHappen; -use PHPUnit\Framework\TestCase; /** * @internal @@ -19,7 +18,7 @@ final class ChainableClosure public static function from(Closure $closure, Closure $next): Closure { return function () use ($closure, $next): void { - if (! is_object($this)) { // @phpstan-ignore-line + if (!is_object($this)) { // @phpstan-ignore-line throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); } diff --git a/src/Support/HigherOrderTapProxy.php b/src/Support/HigherOrderTapProxy.php index bfce21c8..9bcfb479 100644 --- a/src/Support/HigherOrderTapProxy.php +++ b/src/Support/HigherOrderTapProxy.php @@ -13,7 +13,7 @@ use Throwable; */ final class HigherOrderTapProxy { - private const UNDEFINED_PROPERTY = 'Undefined property: P\\'; + private const UNDEFINED_PROPERTY = 'Undefined property: P\\'; // @phpstan-ignore-line /** * Create a new tap proxy instance. @@ -42,9 +42,8 @@ final class HigherOrderTapProxy public function __get(string $property) { try { - /** @throws Throwable */ return $this->target->{$property}; // @phpstan-ignore-line - } catch (Throwable $throwable) { + } catch (Throwable $throwable) { // @phpstan-ignore-line Reflection::setPropertyValue($throwable, 'file', Backtrace::file()); Reflection::setPropertyValue($throwable, 'line', Backtrace::line()); From f6004e07c1175711cdaf60af4f5988708b1b909f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 14 Nov 2021 21:45:13 +0000 Subject: [PATCH 17/31] chore: ignores windows builds --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fce3d953..9245e846 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest] # (macos-latest, windows-latest) 2.x-dev is under development php: ['8.0', '8.1'] dependency-version: [prefer-lowest, prefer-stable] parallel: ['', '--parallel'] From 22895ce6826799b0aa98ee301a1f0ec998b6b51d Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 17 Nov 2021 10:55:45 +0000 Subject: [PATCH 18/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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] ?? []); } /**