Merge branch 'master' into next-1

This commit is contained in:
Fabio Ivona
2021-11-29 09:36:07 +01:00
committed by GitHub
16 changed files with 274 additions and 97 deletions

View File

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

View File

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

View File

@ -9,7 +9,6 @@ parameters:
- src
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: false
reportUnmatchedIgnoredErrors: true
ignoreErrors:

View File

@ -14,6 +14,8 @@ trait RetrievesValues
*
* Safely retrieve the value at the given key from an object or array.
*
* @template TRetrievableValue
*
* @param array<string, TRetrievableValue>|object $value
* @param TRetrievableValue|null $default
*

View File

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

View File

@ -7,7 +7,9 @@ namespace Pest;
/**
* @internal
*
* @mixin Expectation
* @template TValue
*
* @mixin Expectation<TValue>
*/
final class Each
{
@ -15,14 +17,21 @@ final class Each
/**
* Creates an expectation on each item of the iterable "value".
*
* @param Expectation<TValue> $original
*/
public function __construct(private Expectation $original)
{
// ..
}
/**
* Creates a new expectation.
*
* @template TAndValue
*
* @param TAndValue $value
*
* @return Expectation<TAndValue>
*/
public function and(mixed $value): Expectation
{
@ -31,6 +40,8 @@ final class Each
/**
* Creates the opposite expectation for the value.
*
* @return self<TValue>
*/
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<int|string, mixed> $arguments
*
* @return self<TValue>
*/
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<TValue>
*/
public function __get(string $name): Each
{

View File

@ -45,9 +45,11 @@ final class Expectation
/**
* Creates a new expectation.
*
* @param TValue $value
* @template TAndValue
*
* @return Expectation<TValue>
* @param TAndValue $value
*
* @return self<TAndValue>
*/
public function and(mixed $value): Expectation
{
@ -56,6 +58,8 @@ final class Expectation
/**
* Creates a new expectation with the decoded JSON value.
*
* @return self<mixed>
*/
public function json(): Expectation
{
@ -84,6 +88,8 @@ final class Expectation
/**
* Send the expectation value to Ray along with all given arguments.
*
* @return self<TValue>
*/
public function ray(mixed ...$arguments): self
{
@ -96,6 +102,8 @@ final class Expectation
/**
* Creates the opposite expectation for the value.
*
* @return OppositeExpectation<TValue>
*/
public function not(): OppositeExpectation
{
@ -104,6 +112,8 @@ final class Expectation
/**
* Creates an expectation on each item of the iterable "value".
*
* @return Each<TValue>
*/
public function each(callable $callback = null): Each
{
@ -125,7 +135,9 @@ final class Expectation
*
* @template TSequenceValue
*
* @param (callable(self, self): void)|TSequenceValue ...$callbacks
* @param (callable(self<TValue>, self<string|int>): void)|TSequenceValue ...$callbacks
*
* @return self<TValue>
*/
public function sequence(mixed ...$callbacks): Expectation
{
@ -167,15 +179,13 @@ final class Expectation
* @template TMatchSubject of array-key
*
* @param (callable(): TMatchSubject)|TMatchSubject $subject
* @param array<TMatchSubject, (callable(Expectation<TValue>): mixed)|TValue> $expressions
* @param array<TMatchSubject, (callable(self<TValue>): mixed)|TValue> $expressions
*
* @return self<TValue>
*/
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;
@ -208,6 +218,8 @@ final class Expectation
*
* @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback
*
* @return self<TValue>
*/
public function unless(callable|bool $condition, callable $callback): Expectation
{
@ -224,7 +236,9 @@ final class Expectation
* Apply the callback if the given "condition" is truthy.
*
* @param (callable(): bool)|bool $condition
* @param callable(Expectation<TValue>): mixed $callback
* @param callable(self<TValue>): mixed $callback
*
* @return self<TValue>
*/
public function when(callable|bool $condition, callable $callback): Expectation
{

View File

@ -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;
@ -73,9 +72,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);
@ -141,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 = <<<EOF
public function $dataProviderName()
{
return __PestDatasets::get(self::\$__filename, "$methodName");
}
EOF;
}
$annotations = implode('', array_map(
static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
));
return <<<EOF
/**$annotations
*/
public function $methodName()
{
return \$this->__runTest(
\$this->__test,
...func_get_args(),
);
}
$datasetsCode
EOF;
}, $methods));
$methodsCode = implode('', array_map(
fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation(self::$annotations),
$methods
));
try {
eval("

View File

@ -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;
@ -17,6 +19,7 @@ use PHPUnit\Framework\TestCase;
final class TestCaseMethodFactory
{
use HigherOrderable;
/**
* Determines if the Test Case will be the "only" being run.
*/
@ -51,11 +54,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();
}
@ -100,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<int, class-string> $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 <<<EOF
/**$annotations
*/
public function $methodName()
{
return \$this->__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 <<<EOF
public function $dataProviderName()
{
return __PestDatasets::get(self::\$__filename, "$methodName");
}
EOF;
}
}

View File

@ -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<TValue>|Extendable
*/
function expect($value = null): Expectation|Extendable
{

View File

@ -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<TOriginalValue>
*/
final class HigherOrderExpectation
{
use Expectable;
use RetrievesValues;
/**
* @var Expectation<TValue>|Each<TValue>
*/
private Expectation|Each $expectation;
private bool $opposite = false;
@ -25,6 +29,9 @@ final class HigherOrderExpectation
/**
* Creates a new higher order expectation.
*
* @param Expectation<TOriginalValue> $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<TOriginalValue, TValue>
*/
public function not(): HigherOrderExpectation
{
@ -41,14 +50,28 @@ final class HigherOrderExpectation
return $this;
}
/**
* Creates a new Expectation.
*
* @template TExpectValue
*
* @param TExpectValue $value
*
* @return Expectation<TExpectValue>
*/
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<TValue>
* @return Expectation<TExpectValue>
*/
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<int, mixed> $arguments
*
* @return self<TOriginalValue, mixed>|self<TOriginalValue, TValue>
*/
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<TOriginalValue, mixed>|self<TOriginalValue, TValue>
*/
public function __get(string $name): self
{
@ -99,9 +126,12 @@ final class HigherOrderExpectation
/**
* Retrieve the applicable value based on the current reset condition.
*
* @return TOriginalValue|TValue
*/
private function getValue(): mixed
{
// @phpstan-ignore-next-line
return $this->shouldReset ? $this->original->value : $this->expectation->value;
}
@ -109,6 +139,8 @@ final class HigherOrderExpectation
* Performs the given assertion with the current expectation.
*
* @param array<int, mixed> $arguments
*
* @return self<TOriginalValue, TValue>
*/
private function performAssertion(string $name, array $arguments): self
{

View File

@ -10,29 +10,34 @@ use SebastianBergmann\Exporter\Exporter;
/**
* @internal
*
* @mixin Expectation
* @template TValue
*
* @mixin Expectation<TValue>
*/
final class OppositeExpectation
{
/**
* Creates a new opposite expectation.
*
* @param Expectation<TValue> $original
*/
public function __construct(private Expectation $original)
{
// ..
}
/**
* Asserts that the value array not has the provided $keys.
*
* @param array<int, int|string> $keys
*
* @return Expectation<TValue>
*/
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<int, mixed> $arguments
*
* @return Expectation|never
* @return Expectation<TValue>|Expectation<mixed>|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<TValue>|Expectation<mixed>|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;
}

View File

@ -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<TValue> $reflection */
$reflection = new ReflectionClass($target);
/* @phpstan-ignore-next-line */
$reflection = $reflection->getParentClass() ?: $reflection;

View File

@ -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<TValue> $reflectionClass */
$reflectionClass = new ReflectionClass($object);
$reflectionProperty = null;
@ -202,4 +205,12 @@ final class Reflection
return $arguments;
}
/**
* @return mixed
*/
public static function getFunctionVariable(Closure $function, string $key)
{
return (new ReflectionFunction($function))->getStaticVariables()[$key] ?? null;
}
}

View File

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

View File

@ -234,6 +234,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) {
@ -243,3 +244,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']; },
]);