diff --git a/src/Factories/Annotations/CoversNothing.php b/src/Factories/Annotations/CoversNothing.php new file mode 100644 index 00000000..8faaca1f --- /dev/null +++ b/src/Factories/Annotations/CoversNothing.php @@ -0,0 +1,30 @@ + $annotations + * + * @return array + */ + public function __invoke(TestCaseMethodFactory $method, array $annotations): array + { + if (($method->covers[0] ?? null) instanceof CoversNothingFactory) { + $annotations[] = '@coversNothing'; + } + + return $annotations; + } +} diff --git a/src/Factories/Attributes/Covers.php b/src/Factories/Attributes/Covers.php index 1c41608b..d6d41c4a 100644 --- a/src/Factories/Attributes/Covers.php +++ b/src/Factories/Attributes/Covers.php @@ -39,8 +39,6 @@ final class Covers extends Attribute $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]"; } } diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index ed3d70e8..7e250eed 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -33,6 +33,7 @@ final class TestCaseFactory private static array $annotations = [ Annotations\Depends::class, Annotations\Groups::class, + Annotations\CoversNothing::class, ]; /** @@ -150,25 +151,31 @@ final class TestCaseFactory $classFQN .= $className; } - $methodsCode = implode('', array_map( - fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation($classFQN, self::$annotations), - $methods - )); + $classAvailableAttributes = array_filter(self::$attributes, fn (string $attribute) => $attribute::ABOVE_CLASS); + $methodAvailableAttributes = array_filter(self::$attributes, fn (string $attribute) => !$attribute::ABOVE_CLASS); - $classAttributesCode = []; - $classAvailableAttributes = array_filter(self::$attributes, fn (string $attribute) => $attribute::ABOVE_CLASS); + $classAttributes = []; foreach ($classAvailableAttributes as $attribute) { - $classAttributesCode = array_reduce( + $classAttributes = array_reduce( $methods, fn (array $carry, TestCaseMethodFactory $methodFactory) => (new $attribute())->__invoke($methodFactory, $carry), - $classAttributesCode + $classAttributes ); } + $methodsCode = implode('', array_map( + fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation( + $classFQN, + self::$annotations, + $methodAvailableAttributes + ), + $methods + )); + $classAttributesCode = implode('', array_map( - static fn (string $attribute) => sprintf("\n %s", $attribute), - array_unique($classAttributesCode), + static fn (string $attribute) => sprintf("\n %s", $attribute), + array_unique($classAttributes), )); try { diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index 21118b8f..83c6678d 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -114,8 +114,9 @@ final class TestCaseMethodFactory * Creates a PHPUnit method as a string ready for evaluation. * * @param array $annotationsToUse + * @param array> $attributesToUse */ - public function buildForEvaluation(string $classFQN, array $annotationsToUse): string + public function buildForEvaluation(string $classFQN, array $annotationsToUse, array $attributesToUse): string { if ($this->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); @@ -129,12 +130,18 @@ final class TestCaseMethodFactory $datasetsCode = ''; $annotations = ['@test']; + $attributes = []; foreach ($annotationsToUse as $annotation) { /** @phpstan-ignore-next-line */ $annotations = (new $annotation())->__invoke($this, $annotations); } + foreach ($attributesToUse as $attribute) { + /** @phpstan-ignore-next-line */ + $attributes = (new $attribute())->__invoke($this, $attributes); + } + if (count($this->datasets) > 0) { $dataProviderName = $methodName . '_dataset'; $annotations[] = "@dataProvider $dataProviderName"; @@ -145,10 +152,15 @@ final class TestCaseMethodFactory static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations, )); + $attributes = implode('', array_map( + static fn ($attribute) => sprintf("\n %s", $attribute), $attributes, + )); + return <<__runTest( diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php index bc125599..48f612b8 100644 --- a/src/PendingCalls/TestCall.php +++ b/src/PendingCalls/TestCall.php @@ -224,7 +224,7 @@ final class TestCall */ public function coversNothing(): TestCall { - $this->testCaseMethod->covers[] = new CoversNothing(); + $this->testCaseMethod->covers = [new CoversNothing()]; return $this; } diff --git a/tests/Features/Covers.php b/tests/Features/Covers.php index 3b9d6809..63c3d8a1 100644 --- a/tests/Features/Covers.php +++ b/tests/Features/Covers.php @@ -35,37 +35,42 @@ it('uses the correct PHPUnit attribute for function', function () { expect($attributes[1]->getArguments()[0])->toBe('foo'); })->coversFunction('foo'); -it('uses the correct PHPUnit attribute for nothing', function () { - $attributes = (new ReflectionClass($this))->getAttributes(); - - expect($attributes[2]->getName())->toBe('PHPUnit\Framework\Attributes\CoversNothing'); -})->coversNothing(); - it('removes duplicated attributes', function () { $attributes = (new ReflectionClass($this))->getAttributes(); + expect($attributes[2]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); + expect($attributes[2]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass2'); expect($attributes[3]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[3]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass2'); - expect($attributes[4]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[4]->getArguments()[0])->toBe('Pest\Factories\Attributes\Covers'); + expect($attributes[3]->getArguments()[0])->toBe('Pest\Factories\Attributes\Covers'); + expect($attributes[4]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); + expect($attributes[4]->getArguments()[0])->toBe('bar'); expect($attributes[5]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); - expect($attributes[5]->getArguments()[0])->toBe('bar'); - expect($attributes[6]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); - expect($attributes[6]->getArguments()[0])->toBe('baz'); + expect($attributes[5]->getArguments()[0])->toBe('baz'); }) ->coversClass(TestCoversClass2::class, TestCoversClass1::class, Covers::class) - ->coversNothing() ->coversFunction('bar', 'foo', 'baz'); it('guesses if the given argument is a class or function', function () { $attributes = (new ReflectionClass($this))->getAttributes(); - expect($attributes[7]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); - expect($attributes[7]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass3'); - expect($attributes[8]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); - expect($attributes[8]->getArguments()[0])->toBe('testCoversFunction'); + expect($attributes[6]->getName())->toBe('PHPUnit\Framework\Attributes\CoversClass'); + expect($attributes[6]->getArguments()[0])->toBe('P\Tests\Features\TestCoversClass3'); + expect($attributes[7]->getName())->toBe('PHPUnit\Framework\Attributes\CoversFunction'); + expect($attributes[7]->getArguments()[0])->toBe('testCoversFunction'); })->covers(TestCoversClass3::class, 'testCoversFunction'); +it('appends CoversNothing to method attributes', function () { + $phpDoc = (new ReflectionClass($this))->getMethod($this->getName()); + + expect(str_contains($phpDoc->getDocComment(), '* @coversNothing'))->toBeTrue(); +})->coversNothing(); + +it('does not append CoversNothing to other methods', function () { + $phpDoc = (new ReflectionClass($this))->getMethod($this->getName()); + + expect(str_contains($phpDoc->getDocComment(), '* @coversNothing'))->toBeFalse(); +}); + it('throws exception if no class nor method has been found', function () { $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');