From eed3ed55138cb3d2e6de9bf81c8e1784315ef653 Mon Sep 17 00:00:00 2001 From: Luke Downing Date: Tue, 19 Oct 2021 21:40:40 +0100 Subject: [PATCH 01/10] Vastly improves the logic around bound datasets to make them more user friendly. --- src/Concerns/Testable.php | 29 +++++++++++++++++++++---- src/Support/Reflection.php | 8 +++++++ tests/.snapshots/success.txt | 13 ++++++++++-- tests/Features/Datasets.php | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 72bf02bb..a6633612 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -7,8 +7,10 @@ namespace Pest\Concerns; use Closure; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; +use Pest\Support\Reflection; use Pest\TestSuite; use PHPUnit\Framework\ExecutionOrderDependency; +use function sprintf; use Throwable; /** @@ -257,7 +259,7 @@ trait Testable */ public function toString(): string { - return \sprintf( + return sprintf( '%s::%s', self::$__filename, $this->__description @@ -283,9 +285,28 @@ trait Testable */ private function resolveTestArguments(array $arguments): array { - return array_map(function ($data) { - return $data instanceof Closure ? $this->__callClosure($data, []) : $data; - }, $arguments); + if (count($arguments) !== 1) { + return $arguments; + } + + if (!$arguments[0] instanceof Closure) { + return $arguments; + } + + $underlyingTest = Reflection::getFunctionVariable($this->__test, 'factoryTest'); + $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); + + if (in_array($testParameterTypes[0], ['Closure', 'callable'])) { + return $arguments; + } + + $boundDatasetResult = $this->__callClosure($arguments[0], []); + + if (count($testParameterTypes) === 1 || !is_array($boundDatasetResult)) { + return [$boundDatasetResult]; + } + + return array_values($boundDatasetResult); } /** diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 44cd754c..afdc1b94 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -204,4 +204,12 @@ final class Reflection return $arguments; } + + /** + * @return mixed + */ + public static function getFunctionVariable(Closure $function, string $key) + { + return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null; + } } diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 0c6a57ab..7bac8198 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -96,11 +96,20 @@ ✓ more than two datasets with (2) / (4) / (5) ✓ more than two datasets with (2) / (4) / (6) ✓ more than two datasets did the job right - ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) + ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #1 + ✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #2 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #1 ✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #2 ✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #1 ✓ it can resolve a dataset after the test case is available with shared array sets with (Closure Object (...)) #2 + ✓ it resolves a potential bound dataset logically with ('foo', Closure Object (...)) + ✓ it resolves a potential bound dataset logically even when the closure comes first with (Closure Object (...), 'bar') + ✓ it will not resolve a closure if it is type hinted as a closure with (Closure Object (...)) #1 + ✓ it will not resolve a closure if it is type hinted as a closure with (Closure Object (...)) #2 + ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #1 + ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2 + ✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...)) + ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...)) PASS Tests\Features\Exceptions ✓ it gives access the the underlying expectException @@ -720,5 +729,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 9 skipped, 478 passed + Tests: 4 incompleted, 9 skipped, 487 passed \ No newline at end of file diff --git a/tests/Features/Datasets.php b/tests/Features/Datasets.php index be94e596..b65f3680 100644 --- a/tests/Features/Datasets.php +++ b/tests/Features/Datasets.php @@ -232,6 +232,7 @@ it('can resolve a dataset after the test case is available', function ($result) expect($result)->toBe('bar'); })->with([ function () { return $this->foo; }, + [function () { return $this->foo; }], ]); it('can resolve a dataset after the test case is available with shared yield sets', function ($result) { @@ -241,3 +242,43 @@ it('can resolve a dataset after the test case is available with shared yield set it('can resolve a dataset after the test case is available with shared array sets', function ($result) { expect($result)->toBeInt()->toBeLessThan(3); })->with('bound.array'); + +it('resolves a potential bound dataset logically', function ($foo, $bar) { + expect($foo)->toBe('foo'); + expect($bar())->toBe('bar'); +})->with([ + ['foo', function () { return 'bar'; }], // This should be passed as a closure because we've passed multiple arguments +]); + +it('resolves a potential bound dataset logically even when the closure comes first', function ($foo, $bar) { + expect($foo())->toBe('foo'); + expect($bar)->toBe('bar'); +})->with([ + [function () { return 'foo'; }, 'bar'], // This should be passed as a closure because we've passed multiple arguments +]); + +it('will not resolve a closure if it is type hinted as a closure', function (Closure $data) { + expect($data())->toBeString(); +})->with([ + function () { return 'foo'; }, + function () { return 'bar'; }, +]); + +it('will not resolve a closure if it is type hinted as a callable', function (callable $data) { + expect($data())->toBeString(); +})->with([ + function () { return 'foo'; }, + function () { return 'bar'; }, +]); + +it('can correctly resolve a bound dataset that returns an array', function (array $data) { + expect($data)->toBe(['foo', 'bar', 'baz']); +})->with([ + function () { return ['foo', 'bar', 'baz']; }, +]); + +it('can correctly resolve a bound dataset that returns an array but wants to be spread', function (string $foo, string $bar, string $baz) { + expect([$foo, $bar, $baz])->toBe(['foo', 'bar', 'baz']); +})->with([ + function () { return ['foo', 'bar', 'baz']; }, +]); From 729d7c4bef5efa48671ce7b8d335d964c64f432e Mon Sep 17 00:00:00 2001 From: luke Date: Sat, 27 Nov 2021 07:44:35 +0000 Subject: [PATCH 02/10] Small tweaks for PHP 8 --- src/Factories/TestCaseFactory.php | 7 ++++--- src/Factories/TestCaseMethodFactory.php | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index f6a61ba2..448734cc 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -73,9 +73,10 @@ final class TestCaseFactory { $methodsUsingOnly = $this->methodsUsingOnly(); - $methods = array_values(array_filter($this->methods, function ($method) use ($methodsUsingOnly) { - return count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true); - })); + $methods = array_values(array_filter( + $this->methods, + fn ($method) => count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true) + )); if (count($methods) > 0) { $this->evaluate($this->filename, $methods); diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 540f2677..5de47009 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\TestCase; final class TestCaseMethodFactory { use HigherOrderable; + /** * Determines if the Test Case will be the "only" being run. */ @@ -51,11 +52,9 @@ final class TestCaseMethodFactory public ?string $description, public ?Closure $closure, ) { - if ($this->closure === null) { - $this->closure = function () { - Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line - }; - } + $this->closure ??= function () { + Assert::getCount() > 0 ?: self::markTestIncomplete(); // @phpstan-ignore-line + }; $this->bootHigherOrderable(); } From 3d0f267a5c02a641477544a38a022af61f2e738d Mon Sep 17 00:00:00 2001 From: luke Date: Sat, 27 Nov 2021 08:08:09 +0000 Subject: [PATCH 03/10] Moves method evaluation to the method factory --- src/Factories/TestCaseFactory.php | 55 ++------------------- src/Factories/TestCaseMethodFactory.php | 66 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 448734cc..b214631b 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -7,7 +7,6 @@ namespace Pest\Factories; use ParseError; use Pest\Concerns; use Pest\Contracts\HasPrintableTestCaseName; -use Pest\Datasets; use Pest\Exceptions\DatasetMissing; use Pest\Exceptions\ShouldNotHappen; use Pest\Exceptions\TestAlreadyExist; @@ -142,56 +141,10 @@ final class TestCaseFactory $classFQN .= $className; } - $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 = ''; - $annotations = ['@test']; - - foreach (self::$annotations as $annotation) { - /** @phpstan-ignore-next-line */ - $annotations = (new $annotation())->__invoke($method, $annotations); - } - - if (count($method->datasets) > 0) { - $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)); + $methodsCode = implode('', array_map( + fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation(self::$annotations), + $methods + )); try { eval(" diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 5de47009..1aaa2dae 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -5,8 +5,10 @@ declare(strict_types=1); namespace Pest\Factories; use Closure; +use Pest\Datasets; use Pest\Exceptions\ShouldNotHappen; use Pest\Factories\Concerns\HigherOrderable; +use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; @@ -99,4 +101,68 @@ final class TestCaseMethodFactory { return count($this->datasets) > 0 || count($this->depends) > 0; } + + /** + * Creates a PHPUnit method as a string ready for evaluation. + * + * @param array $annotationsToUse + */ + public function buildForEvaluation(array $annotationsToUse): string + { + if ($this->description === null) { + throw ShouldNotHappen::fromMessage('The test description may not be empty.'); + } + + $methodName = Str::evaluable($this->description); + + $datasetsCode = ''; + $annotations = ['@test']; + + foreach ($annotationsToUse as $annotation) { + /** @phpstan-ignore-next-line */ + $annotations = (new $annotation())->__invoke($this, $annotations); + } + + if (count($this->datasets) > 0) { + $dataProviderName = $methodName . '_dataset'; + $annotations[] = "@dataProvider $dataProviderName"; + $datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName); + } + + $annotations = implode('', array_map( + static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations, + )); + + return <<__runTest( + \$this->__test, + ...func_get_args(), + ); + } + + $datasetsCode + EOF; + } + + /** + * Creates a PHPUnit Data Provider as a string ready for evaluation. + */ + private function buildDatasetForEvaluation(string $methodName, string $dataProviderName): string + { + Datasets::with($this->filename, $methodName, $this->datasets); + + return << Date: Sat, 27 Nov 2021 18:48:58 +0000 Subject: [PATCH 04/10] Improved generics for higher order --- phpstan.neon | 1 - src/Concerns/RetrievesValues.php | 2 + src/Each.php | 19 +++- src/Expectation.php | 144 ++++++++++++++++++++++++++--- src/Functions.php | 6 +- src/HigherOrderExpectation.php | 43 +++++++-- src/OppositeExpectation.php | 19 ++-- src/Support/HigherOrderMessage.php | 6 +- src/Support/Reflection.php | 7 +- 9 files changed, 212 insertions(+), 35 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index d5dc4ef6..0a1cba5c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,7 +9,6 @@ parameters: - src checkMissingIterableValueType: true - checkGenericClassInNonGenericObjectType: false reportUnmatchedIgnoredErrors: true ignoreErrors: diff --git a/src/Concerns/RetrievesValues.php b/src/Concerns/RetrievesValues.php index 56f3d2c8..c7c6cdd9 100644 --- a/src/Concerns/RetrievesValues.php +++ b/src/Concerns/RetrievesValues.php @@ -14,6 +14,8 @@ trait RetrievesValues * * Safely retrieve the value at the given key from an object or array. * + * @template TRetrievableValue + * * @param array|object $value * @param TRetrievableValue|null $default * diff --git a/src/Each.php b/src/Each.php index e16933be..6a8a499d 100644 --- a/src/Each.php +++ b/src/Each.php @@ -7,7 +7,9 @@ namespace Pest; /** * @internal * - * @mixin Expectation + * @template TEachValue + * + * @mixin Expectation */ final class Each { @@ -15,14 +17,21 @@ final class Each /** * Creates an expectation on each item of the iterable "value". + * + * @param Expectation $original */ public function __construct(private Expectation $original) { - // .. } /** * Creates a new expectation. + * + * @template TValue + * + * @param TValue $value + * + * @return Expectation */ public function and(mixed $value): Expectation { @@ -31,6 +40,8 @@ final class Each /** * Creates the opposite expectation for the value. + * + * @return self */ public function not(): Each { @@ -43,6 +54,8 @@ final class Each * Dynamically calls methods on the class with the given arguments on each item. * * @param array $arguments + * + * @return self */ public function __call(string $name, array $arguments): Each { @@ -58,6 +71,8 @@ final class Each /** * Dynamically calls methods on the class without any arguments on each item. + * + * @return self */ public function __get(string $name): Each { diff --git a/src/Expectation.php b/src/Expectation.php index 54ad80e2..8d47d7dc 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -46,18 +46,18 @@ final class Expectation * * @param TValue $value */ - public function __construct( - public mixed $value - ) { - // .. + public function __construct(public mixed $value) + { } /** * Creates a new expectation. * - * @param TValue $value + * @template TAndValue * - * @return Expectation + * @param TAndValue $value + * + * @return self */ public function and(mixed $value): Expectation { @@ -66,6 +66,8 @@ final class Expectation /** * Creates a new expectation with the decoded JSON value. + * + * @return self */ public function json(): Expectation { @@ -94,6 +96,8 @@ final class Expectation /** * Send the expectation value to Ray along with all given arguments. + * + * @return self */ public function ray(mixed ...$arguments): self { @@ -106,6 +110,8 @@ final class Expectation /** * Creates the opposite expectation for the value. + * + * @return OppositeExpectation */ public function not(): OppositeExpectation { @@ -114,6 +120,8 @@ final class Expectation /** * Creates an expectation on each item of the iterable "value". + * + * @return Each */ public function each(callable $callback = null): Each { @@ -135,7 +143,9 @@ final class Expectation * * @template TSequenceValue * - * @param (callable(self, self): void)|TSequenceValue ...$callbacks + * @param (callable(self, self): void)|TSequenceValue ...$callbacks + * + * @return self */ public function sequence(mixed ...$callbacks): Expectation { @@ -177,15 +187,13 @@ final class Expectation * @template TMatchSubject of array-key * * @param (callable(): TMatchSubject)|TMatchSubject $subject - * @param array): mixed)|TValue> $expressions + * @param array): mixed)|TValue> $expressions + * + * @return self */ public function match(mixed $subject, array $expressions): Expectation { - $subject = is_callable($subject) - ? $subject - : fn () => $subject; - - $subject = $subject(); + $subject = $subject instanceof Closure ? $subject() : $subject; $matched = false; @@ -218,6 +226,8 @@ final class Expectation * * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback + * + * @return self */ public function unless(callable|bool $condition, callable $callback): Expectation { @@ -234,7 +244,9 @@ final class Expectation * Apply the callback if the given "condition" is truthy. * * @param (callable(): bool)|bool $condition - * @param callable(Expectation): mixed $callback + * @param callable(self): mixed $callback + * + * @return self */ public function when(callable|bool $condition, callable $callback): Expectation { @@ -255,6 +267,8 @@ final class Expectation * Asserts that two variables have the same type and * value. Used on objects, it asserts that two * variables reference the same object. + * + * @return self */ public function toBe(mixed $expected): Expectation { @@ -265,6 +279,8 @@ final class Expectation /** * Asserts that the value is empty. + * + * @return self */ public function toBeEmpty(): Expectation { @@ -275,6 +291,8 @@ final class Expectation /** * Asserts that the value is true. + * + * @return self */ public function toBeTrue(): Expectation { @@ -285,6 +303,8 @@ final class Expectation /** * Asserts that the value is truthy. + * + * @return self */ public function toBeTruthy(): Expectation { @@ -295,6 +315,8 @@ final class Expectation /** * Asserts that the value is false. + * + * @return self */ public function toBeFalse(): Expectation { @@ -305,6 +327,8 @@ final class Expectation /** * Asserts that the value is falsy. + * + * @return self */ public function toBeFalsy(): Expectation { @@ -315,6 +339,8 @@ final class Expectation /** * Asserts that the value is greater than $expected. + * + * @return self */ public function toBeGreaterThan(int|float $expected): Expectation { @@ -325,6 +351,8 @@ final class Expectation /** * Asserts that the value is greater than or equal to $expected. + * + * @return self */ public function toBeGreaterThanOrEqual(int|float $expected): Expectation { @@ -335,6 +363,8 @@ final class Expectation /** * Asserts that the value is less than or equal to $expected. + * + * @return self */ public function toBeLessThan(int|float $expected): Expectation { @@ -345,6 +375,8 @@ final class Expectation /** * Asserts that the value is less than $expected. + * + * @return self */ public function toBeLessThanOrEqual(int|float $expected): Expectation { @@ -355,6 +387,8 @@ final class Expectation /** * Asserts that $needle is an element of the value. + * + * @return self */ public function toContain(mixed ...$needles): Expectation { @@ -377,6 +411,8 @@ final class Expectation * Asserts that the value starts with $expected. * * @param non-empty-string $expected + * + * @return self */ public function toStartWith(string $expected): Expectation { @@ -393,6 +429,8 @@ final class Expectation * Asserts that the value ends with $expected. * * @param non-empty-string $expected + * + * @return self */ public function toEndWith(string $expected): Expectation { @@ -407,6 +445,8 @@ final class Expectation /** * Asserts that $number matches value's Length. + * + * @return self */ public function toHaveLength(int $number): Expectation { @@ -437,6 +477,8 @@ final class Expectation /** * Asserts that $count matches the number of elements of the value. + * + * @return self */ public function toHaveCount(int $count): Expectation { @@ -451,6 +493,8 @@ final class Expectation /** * Asserts that the value contains the property $name. + * + * @return self */ public function toHaveProperty(string $name, mixed $value = null): Expectation { @@ -471,6 +515,8 @@ final class Expectation * Asserts that the value contains the provided properties $names. * * @param iterable $names + * + * @return self */ public function toHaveProperties(iterable $names): Expectation { @@ -483,6 +529,8 @@ final class Expectation /** * Asserts that two variables have the same value. + * + * @return self */ public function toEqual(mixed $expected): Expectation { @@ -499,6 +547,8 @@ final class Expectation * are sorted before they are compared. When $expected and $this->value * are objects, each object is converted to an array containing all * private, protected and public attributes. + * + * @return self */ public function toEqualCanonicalizing(mixed $expected): Expectation { @@ -510,6 +560,8 @@ final class Expectation /** * Asserts that the absolute difference between the value and $expected * is lower than $delta. + * + * @return self */ public function toEqualWithDelta(mixed $expected, float $delta): Expectation { @@ -522,6 +574,8 @@ final class Expectation * Asserts that the value is one of the given values. * * @param iterable $values + * + * @return self */ public function toBeIn(iterable $values): Expectation { @@ -532,6 +586,8 @@ final class Expectation /** * Asserts that the value is infinite. + * + * @return self */ public function toBeInfinite(): Expectation { @@ -544,6 +600,8 @@ final class Expectation * Asserts that the value is an instance of $class. * * @param class-string $class + * + * @return self */ public function toBeInstanceOf(string $class): Expectation { @@ -554,6 +612,8 @@ final class Expectation /** * Asserts that the value is an array. + * + * @return self */ public function toBeArray(): Expectation { @@ -564,6 +624,8 @@ final class Expectation /** * Asserts that the value is of type bool. + * + * @return self */ public function toBeBool(): Expectation { @@ -574,6 +636,8 @@ final class Expectation /** * Asserts that the value is of type callable. + * + * @return self */ public function toBeCallable(): Expectation { @@ -584,6 +648,8 @@ final class Expectation /** * Asserts that the value is of type float. + * + * @return self */ public function toBeFloat(): Expectation { @@ -594,6 +660,8 @@ final class Expectation /** * Asserts that the value is of type int. + * + * @return self */ public function toBeInt(): Expectation { @@ -604,6 +672,8 @@ final class Expectation /** * Asserts that the value is of type iterable. + * + * @return self */ public function toBeIterable(): Expectation { @@ -614,6 +684,8 @@ final class Expectation /** * Asserts that the value is of type numeric. + * + * @return self */ public function toBeNumeric(): Expectation { @@ -624,6 +696,8 @@ final class Expectation /** * Asserts that the value is of type object. + * + * @return self */ public function toBeObject(): Expectation { @@ -634,6 +708,8 @@ final class Expectation /** * Asserts that the value is of type resource. + * + * @return self */ public function toBeResource(): Expectation { @@ -644,6 +720,8 @@ final class Expectation /** * Asserts that the value is of type scalar. + * + * @return self */ public function toBeScalar(): Expectation { @@ -654,6 +732,8 @@ final class Expectation /** * Asserts that the value is of type string. + * + * @return self */ public function toBeString(): Expectation { @@ -664,6 +744,8 @@ final class Expectation /** * Asserts that the value is a JSON string. + * + * @return self */ public function toBeJson(): Expectation { @@ -677,6 +759,8 @@ final class Expectation /** * Asserts that the value is NAN. + * + * @return self */ public function toBeNan(): Expectation { @@ -687,6 +771,8 @@ final class Expectation /** * Asserts that the value is null. + * + * @return self */ public function toBeNull(): Expectation { @@ -697,6 +783,8 @@ final class Expectation /** * Asserts that the value array has the provided $key. + * + * @return self */ public function toHaveKey(string|int $key, mixed $value = null): Expectation { @@ -725,6 +813,8 @@ final class Expectation * Asserts that the value array has the provided $keys. * * @param array $keys + * + * @return self */ public function toHaveKeys(array $keys): Expectation { @@ -737,6 +827,8 @@ final class Expectation /** * Asserts that the value is a directory. + * + * @return self */ public function toBeDirectory(): Expectation { @@ -751,6 +843,8 @@ final class Expectation /** * Asserts that the value is a directory and is readable. + * + * @return self */ public function toBeReadableDirectory(): Expectation { @@ -765,6 +859,8 @@ final class Expectation /** * Asserts that the value is a directory and is writable. + * + * @return self */ public function toBeWritableDirectory(): Expectation { @@ -779,6 +875,8 @@ final class Expectation /** * Asserts that the value is a file. + * + * @return self */ public function toBeFile(): Expectation { @@ -793,6 +891,8 @@ final class Expectation /** * Asserts that the value is a file and is readable. + * + * @return self */ public function toBeReadableFile(): Expectation { @@ -807,6 +907,8 @@ final class Expectation /** * Asserts that the value is a file and is writable. + * + * @return self */ public function toBeWritableFile(): Expectation { @@ -822,6 +924,8 @@ final class Expectation * Asserts that the value array matches the given array subset. * * @param iterable $array + * + * @return self */ public function toMatchArray(iterable|object $array): Expectation { @@ -853,6 +957,8 @@ final class Expectation * of the properties of an given object. * * @param iterable|object $object + * + * @return self */ public function toMatchObject(iterable|object $object): Expectation { @@ -881,6 +987,8 @@ final class Expectation /** * Asserts that the value matches a regular expression. + * + * @return self */ public function toMatch(string $expression): Expectation { @@ -894,6 +1002,8 @@ final class Expectation /** * Asserts that the value matches a constraint. + * + * @return self */ public function toMatchConstraint(Constraint $constraint): Expectation { @@ -906,6 +1016,8 @@ final class Expectation * Asserts that executing value throws an exception. * * @param (Closure(Throwable): mixed)|string $exception + * + * @return self */ public function toThrow(callable|string $exception, string $exceptionMessage = null): Expectation { @@ -970,7 +1082,7 @@ final class Expectation * * @param array $parameters * - * @return HigherOrderExpectation|mixed + * @return HigherOrderExpectation|self|mixed */ public function __call(string $method, array $parameters) { @@ -985,6 +1097,8 @@ final class Expectation /** * Dynamically calls methods on the class without any arguments * or creates a new higher order expectation. + * + * @return self|OppositeExpectation|Each|HigherOrderExpectation */ public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation { diff --git a/src/Functions.php b/src/Functions.php index fe1dd39a..80e0d9e0 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -18,7 +18,11 @@ if (!function_exists('expect')) { /** * Creates a new expectation. * - * @param mixed $value the Value + * @template TValue + * + * @param TValue $value the Value + * + * @return Expectation|Extendable */ function expect($value = null): Expectation|Extendable { diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index eb959cd4..ed54e09b 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -4,19 +4,23 @@ declare(strict_types=1); namespace Pest; -use Pest\Concerns\Expectable; use Pest\Concerns\RetrievesValues; /** * @internal * - * @mixin Expectation + * @template TOriginalValue + * @template TValue + * + * @mixin Expectation */ final class HigherOrderExpectation { - use Expectable; use RetrievesValues; + /** + * @var Expectation|Each + */ private Expectation|Each $expectation; private bool $opposite = false; @@ -25,6 +29,9 @@ final class HigherOrderExpectation /** * Creates a new higher order expectation. + * + * @param Expectation $original + * @param TValue $value */ public function __construct(private Expectation $original, mixed $value) { @@ -33,6 +40,8 @@ final class HigherOrderExpectation /** * Creates the opposite expectation for the value. + * + * @return self */ public function not(): HigherOrderExpectation { @@ -41,14 +50,28 @@ final class HigherOrderExpectation return $this; } + /** + * Creates a new Expectation. + * + * @template TExpectValue + * + * @param TExpectValue $value + * + * @return Expectation + */ + public function expect(mixed $value): Expectation + { + return new Expectation($value); + } + /** * Creates a new expectation. * - * @template TValue + * @template TExpectValue * - * @param TValue $value + * @param TExpectValue $value * - * @return Expectation + * @return Expectation */ public function and(mixed $value): Expectation { @@ -59,6 +82,8 @@ final class HigherOrderExpectation * Dynamically calls methods on the class with the given arguments. * * @param array $arguments + * + * @return self|self */ public function __call(string $name, array $arguments): self { @@ -72,6 +97,8 @@ final class HigherOrderExpectation /** * Accesses properties in the value or in the expectation. + * + * @return self|self */ public function __get(string $name): self { @@ -99,6 +126,8 @@ final class HigherOrderExpectation /** * Retrieve the applicable value based on the current reset condition. + * + * @return TOriginalValue|TValue */ private function getValue(): mixed { @@ -109,6 +138,8 @@ final class HigherOrderExpectation * Performs the given assertion with the current expectation. * * @param array $arguments + * + * @return self */ private function performAssertion(string $name, array $arguments): self { diff --git a/src/OppositeExpectation.php b/src/OppositeExpectation.php index da9ec6ab..e2062e75 100644 --- a/src/OppositeExpectation.php +++ b/src/OppositeExpectation.php @@ -10,29 +10,34 @@ use SebastianBergmann\Exporter\Exporter; /** * @internal * - * @mixin Expectation + * @template TValue + * + * @mixin Expectation */ final class OppositeExpectation { /** * Creates a new opposite expectation. + * + * @param Expectation $original */ public function __construct(private Expectation $original) { - // .. } /** * Asserts that the value array not has the provided $keys. * * @param array $keys + * + * @return Expectation */ public function toHaveKeys(array $keys): Expectation { foreach ($keys as $key) { try { $this->original->toHaveKey($key); - } catch (ExpectationFailedException $exception) { + } catch (ExpectationFailedException) { continue; } @@ -47,14 +52,14 @@ final class OppositeExpectation * * @param array $arguments * - * @return Expectation|never + * @return Expectation|Expectation|never */ public function __call(string $name, array $arguments): Expectation { try { /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); - } catch (ExpectationFailedException $exception) { + } catch (ExpectationFailedException) { return $this->original; } @@ -64,13 +69,13 @@ final class OppositeExpectation /** * Handle dynamic properties gets into the original expectation. * - * @return Expectation|never + * @return Expectation|Expectation|never */ public function __get(string $name): Expectation { try { $this->original->{$name}; // @phpstan-ignore-line - } catch (ExpectationFailedException $exception) { // @phpstan-ignore-line + } catch (ExpectationFailedException) { // @phpstan-ignore-line return $this->original; } diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index 575eaf98..766a9743 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -38,6 +38,10 @@ final class HigherOrderMessage /** * Re-throws the given `$throwable` with the good line and filename. + * + * @template TValue of object + * + * @param TValue $target */ public function call(object $target): mixed { @@ -59,7 +63,7 @@ final class HigherOrderMessage Reflection::setPropertyValue($throwable, 'line', $this->line); if ($throwable->getMessage() === self::getUndefinedMethodMessage($target, $this->name)) { - /** @var ReflectionClass $reflection */ + /** @var ReflectionClass $reflection */ $reflection = new ReflectionClass($target); /* @phpstan-ignore-next-line */ $reflection = $reflection->getParentClass() ?: $reflection; diff --git a/src/Support/Reflection.php b/src/Support/Reflection.php index 479a86f6..b1d6216b 100644 --- a/src/Support/Reflection.php +++ b/src/Support/Reflection.php @@ -118,11 +118,14 @@ final class Reflection /** * Sets the property value of the given object. * - * @param mixed $value + * @template TValue of object + * + * @param TValue $object + * @param mixed $value */ public static function setPropertyValue(object $object, string $property, $value): void { - /** @var ReflectionClass $reflectionClass */ + /** @var ReflectionClass $reflectionClass */ $reflectionClass = new ReflectionClass($object); $reflectionProperty = null; From d65cc9be842a25fce1cde01da699d84532bb24ae Mon Sep 17 00:00:00 2001 From: luke Date: Sat, 27 Nov 2021 19:20:29 +0000 Subject: [PATCH 05/10] Improved generics for higher order --- src/Each.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Each.php b/src/Each.php index 6a8a499d..19105945 100644 --- a/src/Each.php +++ b/src/Each.php @@ -7,9 +7,9 @@ namespace Pest; /** * @internal * - * @template TEachValue + * @template TValue * - * @mixin Expectation + * @mixin Expectation */ final class Each { @@ -18,7 +18,7 @@ final class Each /** * Creates an expectation on each item of the iterable "value". * - * @param Expectation $original + * @param Expectation $original */ public function __construct(private Expectation $original) { @@ -27,11 +27,11 @@ final class Each /** * Creates a new expectation. * - * @template TValue + * @template TAndValue * - * @param TValue $value + * @param TAndValue $value * - * @return Expectation + * @return Expectation */ public function and(mixed $value): Expectation { @@ -41,7 +41,7 @@ final class Each /** * Creates the opposite expectation for the value. * - * @return self + * @return self */ public function not(): Each { @@ -55,7 +55,7 @@ final class Each * * @param array $arguments * - * @return self + * @return self */ public function __call(string $name, array $arguments): Each { @@ -72,7 +72,7 @@ final class Each /** * Dynamically calls methods on the class without any arguments on each item. * - * @return self + * @return self */ public function __get(string $name): Each { From 7a0e841a0d6684289939698d4fbe3233c3ac23f9 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 Nov 2021 19:41:50 +0000 Subject: [PATCH 06/10] chore: fixes tests --- composer.json | 4 ++-- src/HigherOrderExpectation.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8b4bdf03..4d50f7b0 100644 --- a/composer.json +++ b/composer.json @@ -60,8 +60,8 @@ "bin/pest" ], "scripts": { - "lint": "php-cs-fixer fix -v", - "test:lint": "php-cs-fixer fix -v --dry-run", + "lint": "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix -v", + "test:lint": "PHP_CS_FIXER_IGNORE_ENV=true php-cs-fixer fix -v --dry-run", "test:types": "phpstan analyse --ansi --memory-limit=-1 --debug", "test:unit": "php bin/pest --colors=always --exclude-group=integration", "test:parallel": "exit 1", diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index ed54e09b..a760e845 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -131,6 +131,7 @@ final class HigherOrderExpectation */ private function getValue(): mixed { + // @phpstan-ignore-next-line return $this->shouldReset ? $this->original->value : $this->expectation->value; } From 32c2df044448f55ed97c46123f854004f41eb908 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 Nov 2021 19:44:41 +0000 Subject: [PATCH 07/10] chore: runs static workflows on php81 --- .github/workflows/static.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index d00ba6ed..e13355b2 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -15,7 +15,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 tools: composer:v2 coverage: none From e236bf3821a791bb24f9ef9c5ad410ebcb6515db Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 Nov 2021 19:51:58 +0000 Subject: [PATCH 08/10] chore: runs static workflows on php81 --- .github/workflows/static.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e13355b2..dec531a2 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -40,7 +40,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 tools: composer:v2 coverage: none @@ -48,4 +48,4 @@ jobs: run: composer update --prefer-stable --no-interaction --no-progress - name: Run PHPStan - run: vendor/bin/phpstan analyse --no-progress + run: vendor/bin/phpstan analyse --no-progress --debug From 8047ae570d3009a24f7b1ac84cb46d1a2fdb850b Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 27 Nov 2021 19:54:21 +0000 Subject: [PATCH 09/10] chore: fixes cs --- .github/workflows/static.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index dec531a2..09e0d064 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -23,7 +23,7 @@ jobs: run: composer update --no-interaction --no-progress - name: Run PHP-CS-Fixer - run: vendor/bin/php-cs-fixer fix -v --allow-risky=yes --dry-run + run: PHP_CS_FIXER_IGNORE_ENV=true vendor/bin/php-cs-fixer fix -v --allow-risky=yes --dry-run phpstan: runs-on: ubuntu-latest From 33d1579660d026d9fd781bc6b68247772bb24051 Mon Sep 17 00:00:00 2001 From: luke Date: Sat, 27 Nov 2021 19:54:39 +0000 Subject: [PATCH 10/10] Updates from main --- src/Concerns/Testable.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index 76e8bc88..e45f1956 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -7,6 +7,7 @@ namespace Pest\Concerns; use Closure; use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; +use Pest\Support\Reflection; use Pest\TestSuite; use Throwable; @@ -210,7 +211,28 @@ trait Testable */ private function __resolveTestArguments(array $arguments): array { - return array_map(fn ($data) => $data instanceof Closure ? $this->__callClosure($data, []) : $data, $arguments); + if (count($arguments) !== 1) { + return $arguments; + } + + if (!$arguments[0] instanceof Closure) { + return $arguments; + } + + $underlyingTest = Reflection::getFunctionVariable($this->__test, 'closure'); + $testParameterTypes = array_values(Reflection::getFunctionArguments($underlyingTest)); + + if (in_array($testParameterTypes[0], ['Closure', 'callable'])) { + return $arguments; + } + + $boundDatasetResult = $this->__callClosure($arguments[0], []); + + if (count($testParameterTypes) === 1 || !is_array($boundDatasetResult)) { + return [$boundDatasetResult]; + } + + return array_values($boundDatasetResult); } /**