diff --git a/composer.json b/composer.json index 973068f5..9e218f4d 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "require-dev": { "pestphp/pest-dev-tools": "^3.0.0", "pestphp/pest-plugin-type-coverage": "^3.0.0", - "symfony/process": "^7.0.0" + "symfony/process": "^7.0.2" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php index b9000ce8..c6fb49af 100644 --- a/src/Concerns/Testable.php +++ b/src/Concerns/Testable.php @@ -10,6 +10,7 @@ use Pest\Support\ChainableClosure; use Pest\Support\ExceptionTrace; use Pest\Support\Reflection; use Pest\TestSuite; +use PHPUnit\Framework\Attributes\PostCondition; use PHPUnit\Framework\TestCase; use ReflectionException; use ReflectionFunction; @@ -337,7 +338,7 @@ trait Testable return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments)); } - /** @postCondition */ + #[PostCondition] protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void { if (count($this->__snapshotChanges) === 0) { diff --git a/src/Contracts/AddsAnnotations.php b/src/Contracts/AddsAnnotations.php deleted file mode 100644 index 5d2452d1..00000000 --- a/src/Contracts/AddsAnnotations.php +++ /dev/null @@ -1,21 +0,0 @@ - $annotations - * @return array - */ - public function __invoke(TestCaseMethodFactory $method, array $annotations): array; -} diff --git a/src/Evaluators/Attributes.php b/src/Evaluators/Attributes.php new file mode 100644 index 00000000..67a39cf3 --- /dev/null +++ b/src/Evaluators/Attributes.php @@ -0,0 +1,31 @@ +name; + + if ($attribute->arguments === []) { + return " #[\\{$name}]"; + } + + $arguments = array_map(fn (string $argument): string => var_export($argument, true), $attribute->arguments); + + return sprintf(' #[\\%s(%s)]', $name, implode(', ', $arguments)); + }, $attributes)); + } +} diff --git a/src/Factories/Annotations/Depends.php b/src/Factories/Annotations/Depends.php deleted file mode 100644 index 2da826ea..00000000 --- a/src/Factories/Annotations/Depends.php +++ /dev/null @@ -1,29 +0,0 @@ -depends as $depend) { - $depend = Str::evaluable($method->describing !== null ? Str::describe($method->describing, $depend) : $depend); - - $annotations[] = "@depends $depend"; - } - - return $annotations; - } -} diff --git a/src/Factories/Annotations/Groups.php b/src/Factories/Annotations/Groups.php deleted file mode 100644 index 6c54afd7..00000000 --- a/src/Factories/Annotations/Groups.php +++ /dev/null @@ -1,26 +0,0 @@ -groups as $group) { - $annotations[] = "@group $group"; - } - - return $annotations; - } -} diff --git a/src/Factories/Annotations/TestDox.php b/src/Factories/Annotations/TestDox.php deleted file mode 100644 index e55738e7..00000000 --- a/src/Factories/Annotations/TestDox.php +++ /dev/null @@ -1,30 +0,0 @@ -description !== null); - $methodDescription = str_replace('*/', '{@*}', $method->description); - - $annotations[] = "@testdox $methodDescription"; - - return $annotations; - } -} diff --git a/src/Factories/Attribute.php b/src/Factories/Attribute.php new file mode 100644 index 00000000..005d8161 --- /dev/null +++ b/src/Factories/Attribute.php @@ -0,0 +1,19 @@ + $arguments + */ + public function __construct(public string $name, public iterable $arguments = []) + { + // + } +} diff --git a/src/Factories/Attributes/Attribute.php b/src/Factories/Attributes/Attribute.php deleted file mode 100644 index e8af0e77..00000000 --- a/src/Factories/Attributes/Attribute.php +++ /dev/null @@ -1,27 +0,0 @@ - $attributes - * @return array - */ - public function __invoke(TestCaseMethodFactory $method, array $attributes): array // @phpstan-ignore-line - { - return $attributes; - } -} diff --git a/src/Factories/Attributes/Covers.php b/src/Factories/Attributes/Covers.php deleted file mode 100644 index ad22ff8a..00000000 --- a/src/Factories/Attributes/Covers.php +++ /dev/null @@ -1,46 +0,0 @@ - $attributes - * @return array - */ - public function __invoke(TestCaseMethodFactory $method, array $attributes): array - { - foreach ($method->covers as $covering) { - if ($covering instanceof CoversClass) { - // Prepend a backslash for FQN classes - if (str_contains($covering->class, '\\')) { - $covering->class = '\\'.$covering->class; - } - - $attributes[] = "#[\PHPUnit\Framework\Attributes\CoversClass({$covering->class}::class)]"; - } elseif ($covering instanceof CoversFunction) { - $attributes[] = "#[\PHPUnit\Framework\Attributes\CoversFunction('{$covering->function}')]"; - } else { - $attributes[] = "#[\PHPUnit\Framework\Attributes\CoversNothing()]"; - } - } - - return $attributes; - } -} diff --git a/src/Factories/Covers/CoversClass.php b/src/Factories/Covers/CoversClass.php deleted file mode 100644 index 44f58487..00000000 --- a/src/Factories/Covers/CoversClass.php +++ /dev/null @@ -1,15 +0,0 @@ -> - */ - private const ANNOTATIONS = [ - Annotations\Depends::class, - Annotations\Groups::class, - Annotations\TestDox::class, - ]; - /** * The list of attributes. * - * @var array> + * @var iterable */ - private const ATTRIBUTES = [ - Attributes\Covers::class, - ]; + public iterable $attributes = []; /** * The FQN of the Test Case class. @@ -145,32 +132,21 @@ final class TestCaseFactory $className = 'InvalidTestName'.Str::random(); } - $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above); - $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above); + $this->attributes = [ + new Attribute( + \PHPUnit\Framework\Attributes\TestDox::class, + [$this->filename], + ), + ...$this->attributes, + ]; - $classAttributes = []; - - foreach ($classAvailableAttributes as $attribute) { - $classAttributes = array_reduce( - $methods, - fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute())->__invoke($methodFactory, $carry), - $classAttributes - ); - } + $attributesCode = Attributes::code($this->attributes); $methodsCode = implode('', array_map( - fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation( - self::ANNOTATIONS, - $methodAvailableAttributes - ), + fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(), $methods )); - $classAttributesCode = implode('', array_map( - static fn (string $attribute): string => sprintf("\n%s", $attribute), - array_unique($classAttributes), - )); - try { $classCode = <<> + */ + public array $attributes = []; + /** * The test's describing, if any. */ @@ -57,13 +65,6 @@ final class TestCaseMethodFactory */ public array $groups = []; - /** - * The covered classes and functions. - * - * @var array - */ - public array $covers = []; - /** * Creates a new test case method factory instance. */ @@ -121,11 +122,8 @@ final class TestCaseMethodFactory /** * Creates a PHPUnit method as a string ready for evaluation. - * - * @param array> $annotationsToUse - * @param array> $attributesToUse */ - public function buildForEvaluation(array $annotationsToUse, array $attributesToUse): string + public function buildForEvaluation(): string { if ($this->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); @@ -134,36 +132,42 @@ final class TestCaseMethodFactory $methodName = Str::evaluable($this->description); $datasetsCode = ''; - $annotations = ['@test']; - $attributes = []; - foreach ($annotationsToUse as $annotation) { - $annotations = (new $annotation())->__invoke($this, $annotations); - } + // prepend attribute + $this->attributes = [ + new Attribute( + \PHPUnit\Framework\Attributes\Test::class, + [], + ), + new Attribute( + \PHPUnit\Framework\Attributes\TestDox::class, + [str_replace('*/', '{@*}', $this->description)], + ), + ...$this->attributes, + ]; - foreach ($attributesToUse as $attribute) { - $attributes = (new $attribute())->__invoke($this, $attributes); + foreach ($this->depends as $depend) { + $depend = Str::evaluable($this->describing !== null ? Str::describe($this->describing, $depend) : $depend); + + $this->attributes[] = new Attribute( + \PHPUnit\Framework\Attributes\Depends::class, + [$depend], + ); } if ($this->datasets !== [] || $this->repetitions > 1) { $dataProviderName = $methodName.'_dataset'; - $annotations[] = "@dataProvider $dataProviderName"; + $this->attributes[] = new Attribute( + DataProvider::class, + [$dataProviderName], + ); $datasetsCode = $this->buildDatasetForEvaluation($methodName, $dataProviderName); } - $annotations = implode('', array_map( - static fn (string $annotation): string => sprintf("\n * %s", $annotation), $annotations, - )); - - $attributes = implode('', array_map( - static fn (string $attribute): string => sprintf("\n %s", $attribute), $attributes, - )); + $attributesCode = Attributes::code($this->attributes); return <<tests->get(self::\$__filename)->getMethod(\$this->name())->getClosure(\$this); @@ -173,7 +177,7 @@ final class TestCaseMethodFactory ...func_get_args(), ); } - $datasetsCode + $datasetsCode PHP; } diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index f16ce913..745f3f2e 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -6,9 +6,7 @@ namespace Pest\PendingCalls; use Closure; use Pest\Exceptions\InvalidArgumentException; -use Pest\Factories\Covers\CoversClass; -use Pest\Factories\Covers\CoversFunction; -use Pest\Factories\Covers\CoversNothing; +use Pest\Factories\Attribute; use Pest\Factories\TestCaseMethodFactory; use Pest\PendingCalls\Concerns\Describable; use Pest\Plugins\Only; @@ -30,6 +28,13 @@ final class TestCall { use Describable; + /** + * The list of test case factory attributes. + * + * @var array + */ + private array $testCaseFactoryAttributes = []; + /** * The Test Case Factory. */ @@ -165,7 +170,10 @@ final class TestCall public function group(string ...$groups): self { foreach ($groups as $group) { - $this->testCaseMethod->groups[] = $group; + $this->testCaseMethod->attributes[] = new Attribute( + \PHPUnit\Framework\Attributes\Group::class, + [$group], + ); } return $this; @@ -321,7 +329,10 @@ final class TestCall public function coversClass(string ...$classes): self { foreach ($classes as $class) { - $this->testCaseMethod->covers[] = new CoversClass($class); + $this->testCaseFactoryAttributes[] = new Attribute( + \PHPUnit\Framework\Attributes\CoversClass::class, + [$class], + ); } return $this; @@ -333,7 +344,10 @@ final class TestCall public function coversFunction(string ...$functions): self { foreach ($functions as $function) { - $this->testCaseMethod->covers[] = new CoversFunction($function); + $this->testCaseFactoryAttributes[] = new Attribute( + \PHPUnit\Framework\Attributes\CoversFunction::class, + [$function], + ); } return $this; @@ -344,7 +358,10 @@ final class TestCall */ public function coversNothing(): self { - $this->testCaseMethod->covers = [new CoversNothing()]; + $this->testCaseMethod->attributes[] = new Attribute( + \PHPUnit\Framework\Attributes\CoversNothing::class, + [], + ); return $this; } @@ -417,5 +434,9 @@ final class TestCall } $this->testSuite->tests->set($this->testCaseMethod); + + foreach ($this->testCaseFactoryAttributes as $attribute) { + $this->testSuite->tests->get($this->filename)->attributes[] = $attribute; + } } } diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index 93fd2338..46f60a9a 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -9,9 +9,11 @@ use Pest\Contracts\TestCaseFilter; use Pest\Contracts\TestCaseMethodFilter; use Pest\Exceptions\TestCaseAlreadyInUse; use Pest\Exceptions\TestCaseClassOrTraitNotFound; +use Pest\Factories\Attribute; use Pest\Factories\TestCaseFactory; use Pest\Factories\TestCaseMethodFactory; use Pest\Support\Str; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; /** @@ -181,14 +183,13 @@ final class TestRepository foreach ($testCase->methods as $method) { foreach ($groups as $group) { - $method->groups[] = $group; + $method->attributes[] = new Attribute( + Group::class, + [$group], + ); } } - foreach ($testCase->methods as $method) { - $method->groups = [...$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]); diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php index dd4701c4..16f7be39 100644 --- a/tests/Features/Covers.php +++ b/tests/Features/Covers.php @@ -3,8 +3,8 @@ use Pest\PendingCalls\TestCall; use Pest\TestSuite; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversFunction; use Tests\Fixtures\Covers\CoversClass1; -use Tests\Fixtures\Covers\CoversClass2; use Tests\Fixtures\Covers\CoversClass3; use Tests\Fixtures\Covers\CoversTrait; @@ -17,45 +17,39 @@ function testCoversFunction() it('uses the correct PHPUnit attribute for class', function () { $attributes = (new ReflectionClass($this))->getAttributes(); - expect($attributes[0]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[0]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); + expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); + expect($attributes[1]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversClass1'); })->coversClass(CoversClass1::class); it('uses the correct PHPUnit attribute for function', function () { $attributes = (new ReflectionClass($this))->getAttributes(); - expect($attributes[1]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); - expect($attributes[1]->getArguments()[0])->toBe('testCoversFunction'); + expect($attributes[2]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); + expect($attributes[2]->getArguments()[0])->toBe('testCoversFunction'); })->coversFunction('testCoversFunction'); -it('removes duplicated attributes', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[2]->getName())->toBe(CoversClass::class); - expect($attributes[2]->getArguments()[0])->toBe(CoversClass2::class); -}) - ->coversClass(CoversClass2::class, CoversClass1::class) - ->coversFunction('testCoversFunction'); - it('guesses if the given argument is a class or function', function () { $attributes = (new ReflectionClass($this))->getAttributes(); expect($attributes[3]->getName())->toBe(CoversClass::class); expect($attributes[3]->getArguments()[0])->toBe(CoversClass3::class); + + expect($attributes[4]->getName())->toBe(CoversFunction::class); + expect($attributes[4]->getArguments()[0])->toBe('testCoversFunction'); })->covers(CoversClass3::class, 'testCoversFunction'); it('uses the correct PHPUnit attribute for trait', function () { $attributes = (new ReflectionClass($this))->getAttributes(); - expect($attributes[4]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[4]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); + expect($attributes[5]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); + expect($attributes[5]->getArguments()[0])->toBe('Tests\Fixtures\Covers\CoversTrait'); })->coversClass(CoversTrait::class); it('uses the correct PHPUnit attribute for covers nothing', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); + $attributes = (new ReflectionMethod($this, $this->name()))->getAttributes(); - expect($attributes[5]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing'); - expect($attributes[5]->getArguments())->toHaveCount(0); + expect($attributes[2]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing'); + expect($attributes[2]->getArguments())->toHaveCount(0); })->coversNothing(); it('throws exception if no class nor method has been found', function () { diff --git a/tests/PHPUnit/CustomTestCase/ExecutedTest.php b/tests/PHPUnit/CustomTestCase/ExecutedTest.php index 29aed3cc..90cab8c4 100644 --- a/tests/PHPUnit/CustomTestCase/ExecutedTest.php +++ b/tests/PHPUnit/CustomTestCase/ExecutedTest.php @@ -12,7 +12,7 @@ class ExecutedTest extends TestCase { public static $executed = false; - /** @test */ + #[Test] public function testThatGetsExecuted(): void { self::$executed = true; diff --git a/tests/PHPUnit/CustomTestCase/ParentTest.php b/tests/PHPUnit/CustomTestCase/ParentTest.php index 6ef967d4..0db9c953 100644 --- a/tests/PHPUnit/CustomTestCase/ParentTest.php +++ b/tests/PHPUnit/CustomTestCase/ParentTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\CustomTestCase; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use function PHPUnit\Framework\assertTrue; @@ -15,7 +16,7 @@ class ParentTest extends TestCase return false; } - /** @test */ + #[Test] public function testOverrideMethod(): void { assertTrue($this->getEntity() || true);