From ace4bcf7e64fdb1eccb47117b860418473ae7d2d Mon Sep 17 00:00:00 2001 From: Eugene Leonovich Date: Wed, 10 Jun 2026 13:38:31 +0200 Subject: [PATCH] Migrate to PHP 8.2, PHPUnit 10 and native attributes --- .gitattributes | 2 +- .github/workflows/qa.yml | 24 +-- .php_cs.dist => .php-cs-fixer.dist.php | 4 +- LICENSE | 2 +- README.md | 174 ++++++++++-------- composer.json | 25 ++- phpunit-extension.xml | 13 +- phpunit.xml.dist | 11 +- psalm.xml | 20 +- src/Annotation/AnnotationExtension.php | 16 +- src/Annotation/AnnotationProcessor.php | 4 +- src/Annotation/AnnotationProcessorBuilder.php | 30 +-- src/Annotation/AnnotationSubscriber.php | 42 +++++ src/Annotation/Annotations.php | 40 ++-- .../Attribute/AnnotationAttribute.php | 21 +++ src/Annotation/Attribute/Requires.php | 39 ++++ src/Annotation/EstablishedAnnotationNames.php | 72 -------- src/Annotation/InvalidAnnotationException.php | 13 +- .../PlaceholderResolver/ChainResolver.php | 2 + .../TargetClassResolver.php | 2 + .../TargetMethodResolver.php | 2 + .../PlaceholderResolver/TmpDirResolver.php | 2 + .../Processor/RequiresProcessor.php | 2 + src/Annotation/ProcessorMap.php | 20 +- .../Requirement/ConditionFunctionProvider.php | 1 + .../Requirement/ConditionRequirement.php | 8 +- .../Requirement/ConstantRequirement.php | 4 +- .../Requirement/PackageRequirement.php | 7 +- src/Annotation/Target.php | 10 +- src/Expectation/ChainExpectation.php | 1 + src/Expectation/Expectations.php | 3 +- src/Expectation/ExpressionExpectation.php | 1 + src/Expectation/IsTruthyExpression.php | 23 +-- src/TestCase.php | 18 +- tests/Annotation/AnnotationExtensionTest.php | 14 +- tests/Annotation/AnnotationProcessorTest.php | 31 ---- tests/Annotation/AnnotationTestCaseTest.php | 14 +- tests/Annotation/Attribute/RequiresTest.php | 41 +++++ tests/Annotation/Log.php | 37 ++++ .../TargetClassResolverTest.php | 11 +- .../TargetMethodResolverTest.php | 11 +- .../TmpDirResolverTest.php | 7 +- .../Processor/RequiresProcessorTest.php | 10 +- .../Requirement/ConditionRequirementTest.php | 7 +- .../Expectation/ExpressionExpectationTest.php | 3 - tests/PHPUnitCompat.php | 27 --- 46 files changed, 451 insertions(+), 420 deletions(-) rename .php_cs.dist => .php-cs-fixer.dist.php (97%) create mode 100644 src/Annotation/AnnotationSubscriber.php create mode 100644 src/Annotation/Attribute/AnnotationAttribute.php create mode 100644 src/Annotation/Attribute/Requires.php delete mode 100644 src/Annotation/EstablishedAnnotationNames.php create mode 100644 tests/Annotation/Attribute/RequiresTest.php create mode 100644 tests/Annotation/Log.php delete mode 100644 tests/PHPUnitCompat.php diff --git a/.gitattributes b/.gitattributes index 008df5f..c52bf58 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,7 @@ tests export-ignore .gitattributes export-ignore .gitignore export-ignore -.php_cs.dist export-ignore +.php-cs-fixer.dist.php export-ignore phpunit.xml.dist export-ignore phpunit-extension.xml export-ignore psalm.xml export-ignore diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index a9d54fb..a896027 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -11,11 +11,11 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['8.2', '8.3', '8.4'] runs-on: ${{ matrix.operating-system }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -25,10 +25,10 @@ jobs: - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -47,20 +47,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: latest + php-version: '8.2' coverage: none - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -77,20 +77,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: '8.2' coverage: none - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 97% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 6de1a73..a9320c6 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -15,7 +15,9 @@ file that was distributed with this source code. EOF; -return Config::create() +$config = new Config(); + +return $config ->setUsingCache(false) ->setRiskyAllowed(true) ->setRules([ diff --git a/LICENSE b/LICENSE index 7727906..51b20dc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2022 Eugene Leonovich +Copyright (c) 2020-2026 Eugene Leonovich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index bfda5f2..e531ec2 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,22 @@ [![Quality Assurance](https://github.com/rybakit/phpunit-extras/workflows/QA/badge.svg)](https://github.com/rybakit/phpunit-extras/actions?query=workflow%3AQA) This repository contains functionality that makes it easy to create and integrate -your own annotations and expectations into the [PHPUnit](https://phpunit.de/) framework. +your own attributes and expectations into the [PHPUnit](https://phpunit.de/) framework. In other words, with this library, your tests may look like this: ![https://raw.githubusercontent.com/rybakit/phpunit-extras/media/phpunit-extras-example.png](../media/phpunit-extras-example.png?raw=true) where: 1. `MySqlServer ^5.6|^8.0` is a custom requirement -2. `@sql` is a custom annotation -3. `%target_method%` is an annotation placeholder +2. `#[Sql(...)]` is a custom attribute +3. `%target_method%` is an attribute placeholder 4. `expectSelectStatementToBeExecutedOnce()` is a custom expectation. ## Table of contents * [Installation](#installation) - * [Annotations](#annotations) + * [Attributes](#attributes) * [Processors](#processors) * [Requires](#requires) * [Requirements](#requirements) @@ -29,7 +29,7 @@ where: * [TargetClass](#targetclass) * [TargetMethod](#targetmethod) * [TmpDir](#tmpdir) - * [Creating your own annotation](#creating-your-own-annotation) + * [Creating your own attribute](#creating-your-own-attribute) * [Expectations](#expectations) * [Usage example](#usage-example) * [Advanced example](#advanced-example) @@ -52,7 +52,7 @@ composer require --dev composer/semver *To use the "package" requirement:* ```bash -composer require --dev ocramius/package-versions +composer require --dev composer/package-versions-deprecated ``` *To use expression-based requirements and/or expectations:* @@ -64,14 +64,14 @@ To install everything in one command, run: ```bash composer require --dev rybakit/phpunit-extras \ composer/semver \ - ocramius/package-versions \ + composer/package-versions-deprecated \ symfony/expression-language ``` -## Annotations +## Attributes -PHPUnit supports a variety of annotations, the full list of which can be found [here](https://phpunit.readthedocs.io/en/latest/annotations.html). +PHPUnit supports a variety of attributes, the full list of which can be found in the PHPUnit manual. With this library, you can easily expand this list by using one of the following options: #### Inheriting from the base test case class @@ -89,15 +89,17 @@ final class MyTest extends TestCase ```php use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\Before; use PHPUnitExtras\Annotation\Annotations; final class MyTest extends TestCase { use Annotations; - protected function setUp() : void + #[Before] + protected function processTestAttributesBeforeTest() : void { - $this->processAnnotations(static::class, $this->getName(false) ?? ''); + $this->processTestAttributes(static::class, $this->name()); } // ... @@ -114,27 +116,26 @@ final class MyTest extends TestCase - + ``` -You can then use annotations provided by the library or created by yourself. +You can then use attributes provided by the library or created by yourself. ### Processors -The annotation processor is a class that implements the behavior of your annotation. +The processor is a class that implements the behavior of your custom attribute. > *The library is currently shipped with only the "Required" processor. -> For inspiration and more examples of annotation processors take a look +> For inspiration and more examples of processors take a look > at the [tarantool/phpunit-extras](https://github.com/tarantool-php/phpunit-extras#processors) package.* #### Requires -This processor extends the standard PHPUnit [@requires](https://phpunit.readthedocs.io/en/latest/annotations.html#requires) -annotation by allowing you to add your own requirements. +This processor lets attributes use the existing requirement registry by passing a requirement type and value. ### Requirements @@ -145,7 +146,7 @@ The library comes with the following requirements: *Format:* ``` -@requires condition +#[Requires('condition', '')] ``` where `` is an arbitrary [expression](https://symfony.com/doc/current/components/expression_language.html#expression-syntax) @@ -155,10 +156,10 @@ in expressions: `cookie`, `env`, `get`, `files`, `post`, `request` and `server`. *Example:* ```php -/** - * @requires condition server.AWS_ACCESS_KEY_ID - * @requires condition server.AWS_SECRET_ACCESS_KEY - */ +use PHPUnitExtras\Annotation\Attribute\Requires; + +#[Requires('condition', 'server.AWS_ACCESS_KEY_ID')] +#[Requires('condition', 'server.AWS_SECRET_ACCESS_KEY')] final class AwsS3AdapterTest extends TestCase { // ... @@ -182,16 +183,16 @@ $annotationProcessorBuilder->addRequirement(new ConditionRequirement($context)); *Format:* ``` -@requires constant +#[Requires('constant', '')] ``` where `` is the constant name. *Example:* ```php -/** - * @requires constant Redis::SERIALIZER_MSGPACK - */ +use PHPUnitExtras\Annotation\Attribute\Requires; + +#[Requires('constant', 'Redis::SERIALIZER_MSGPACK')] public function testSerializeToMessagePack() : void { // ... @@ -203,7 +204,7 @@ public function testSerializeToMessagePack() : void *Format:* ``` -@requires package [] +#[Requires('package', ' []')] ``` where `` is the name of the required package and `` is a composer-like version constraint. For details on supported constraint formats, please refer to the Composer [documentation](https://getcomposer.org/doc/articles/versions.md#writing-version-constraints). @@ -211,9 +212,9 @@ For details on supported constraint formats, please refer to the Composer [docum *Example:* ```php -/** - * @requires package symfony/uid ^5.1 - */ +use PHPUnitExtras\Annotation\Attribute\Requires; + +#[Requires('package', 'symfony/uid ^5.1')] public function testUseUuidAsPrimaryKey() : void { // ... @@ -222,8 +223,8 @@ public function testUseUuidAsPrimaryKey() : void ### Placeholders -Placeholders allow you to dynamically include specific values in your annotations. -The placeholder is any text surrounded by the symbol `%`. An annotation can have +Placeholders allow you to dynamically include specific values in your attribute strings. +The placeholder is any text surrounded by the symbol `%`. An attribute can have any number of placeholders. If the placeholder is unknown, an error will be thrown. Below is a list of the placeholders available by default: @@ -235,10 +236,8 @@ Below is a list of the placeholders available by default: ```php namespace App\Tests; -/** - * @example %target_class% - * @example %target_class_full% - */ +#[Example('%target_class%')] +#[Example('%target_class_full%')] final class FoobarTest extends TestCase { // ... @@ -254,10 +253,8 @@ and `%target_class_full%` will be substituted with `App\Tests\FoobarTest`. *Example:* ```php -/** - * @example %target_method% - * @example %target_method_full% - */ +#[Example('%target_method%')] +#[Example('%target_method_full%')] public function testFoobar() : void { // ... @@ -273,9 +270,7 @@ and `%target_method_full%` will be substituted with `testFoobar`. *Example:* ```php -/** - * @log %tmp_dir%/%target_class%.%target_method%.log testing Foobar - */ +#[Log('%tmp_dir%/%target_class%.%target_method%.log testing Foobar')] public function testFoobar() : void { // ... @@ -286,9 +281,9 @@ In the above example, `%tmp_dir%` will be substituted with the result of the [sys_get_temp_dir()](https://www.php.net/manual/en/function.sys-get-temp-dir.php) call. -### Creating your own annotation +### Creating your own attribute -As an example, let's implement the annotation `@sql` from the picture above. To do this, create a processor class +As an example, let's implement a `#[Sql(...)]` attribute. First, create a processor class with the name `SqlProcessor`: ```php @@ -317,10 +312,38 @@ final class SqlProcessor implements Processor } ``` -That's it. All this processor does is register the `@sql` tag and call `PDO::exec()`, passing everything -that comes after the tag as an argument. In other words, an annotation such as `@sql TRUNCATE TABLE foo` +That's it. All this processor does is register the `sql` processor name and call `PDO::exec()`, passing the attribute value as an argument. In other words, an attribute such as `#[Sql('TRUNCATE TABLE foo')]` is equivalent to `$this->conn->exec('TRUNCATE TABLE foo')`. +Next, create the attribute class that maps `#[Sql(...)]` to the `sql` processor name: + +```php +namespace App\Tests\PhpUnit; + +use PHPUnitExtras\Annotation\Attribute\AnnotationAttribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Sql implements AnnotationAttribute +{ + private string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function getName() : string + { + return 'sql'; + } + + public function getValue() : string + { + return $this->value; + } +} +``` + Also, just for the purpose of example, let's create a placeholder resolver that replaces `%table_name%` with a unique table name for a specific test method or/and class. That will allow using dynamic table names instead of hardcoded ones: @@ -354,7 +377,7 @@ final class TableNameResolver implements PlaceholderResolver } ``` -The only thing left is to register our new annotation: +The only thing left is to register our new processor: ```php namespace App\Tests; @@ -380,12 +403,11 @@ abstract class TestCase extends BaseTestCase } ``` -After that all classes inherited from `App\Tests\TestCase` will be able to use the tag `@sql`. +After that all classes inherited from `App\Tests\TestCase` will be able to use `#[Sql(...)]`. -> *Don't worry if you forgot to inherit from the base class where your annotations are registered -> or if you made a mistake in the annotation name, the library will warn you about an unknown annotation.* +> *If an attribute returns a processor name that was not registered, the library will warn you about unknown metadata.* -As mentioned [earlier](#registering-an-extension), another way to register annotations is through PHPUnit extensions. +As mentioned [earlier](#registering-an-extension), another way to register attributes is through PHPUnit extensions. As in the example above, you need to override the `createAnnotationProcessorBuilder()` method, but now for the `AnnotationExtension` class: @@ -394,15 +416,22 @@ namespace App\Tests\PhpUnit; use PHPUnitExtras\Annotation\AnnotationExtension as BaseAnnotationExtension; use PHPUnitExtras\Annotation\AnnotationProcessorBuilder; +use PHPUnit\Runner\Extension\Facade; +use PHPUnit\Runner\Extension\ParameterCollection; +use PHPUnit\TextUI\Configuration\Configuration; class AnnotationExtension extends BaseAnnotationExtension { - private $dsn; - private $conn; + private string $dsn = 'mysql:host=localhost;dbname=test'; + private ?\PDO $conn = null; - public function __construct($dsn = 'mysql:host=localhost;dbname=test') + public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters) : void { - $this->dsn = $dsn; + if ($parameters->has('dsn')) { + $this->dsn = $parameters->get('dsn'); + } + + parent::bootstrap($configuration, $facade, $parameters); } protected function createAnnotationProcessorBuilder() : AnnotationProcessorBuilder @@ -421,37 +450,34 @@ class AnnotationExtension extends BaseAnnotationExtension After that, register your extension: ```xml - + - - - - -``` + + + + + ``` -To change the default connection settings, pass the new DSN value as an argument: +To change the default connection settings, pass the new DSN value as a parameter: ```xml - - - sqlite::memory: - - + + + ``` -> *For more information on configuring extensions, please follow this [link](https://phpunit.readthedocs.io/en/latest/extending-phpunit.html#configuring-extensions).* +> *For more information on configuring extensions, please refer to the PHPUnit manual.* ## Expectations PHPUnit has a number of methods to set up expectations for code executed under test. Probably the most commonly used -are the [expectException*](https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#testing-exceptions) -and [expectOutput*](https://phpunit.readthedocs.io/en/latest/writing-tests-for-phpunit.html#testing-output) family of methods. +are the `expectException*` and `expectOutput*` family of methods. The library provides the possibility to create your own expectations with ease. diff --git a/composer.json b/composer.json index c2380f1..d14d2c7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "rybakit/phpunit-extras", - "description": "Custom annotations and expectations for PHPUnit.", - "keywords": ["phpunit", "annotations", "extensions", "assertions", "expectations", "custom"], + "description": "Custom attributes and expectations for PHPUnit.", + "keywords": ["phpunit", "attributes", "extensions", "assertions", "expectations", "custom"], "type": "library", "license": "MIT", "authors": [ @@ -11,20 +11,19 @@ } ], "require": { - "php": "^7.1|^8", - "phpunit/phpunit": "^7.1|^8|^9" + "php": ">=8.2", + "phpunit/phpunit": "^10.5" }, "require-dev": { - "php": "^7.1.3|^8", - "composer/semver": "^1.5", - "friendsofphp/php-cs-fixer": "^2.18", - "ocramius/package-versions": "^1.4", - "symfony/expression-language": "^3.3|^4|^5", - "vimeo/psalm": "^3.9|^4" + "composer/package-versions-deprecated": "^1.11", + "composer/semver": "^3.4", + "friendsofphp/php-cs-fixer": "^3.75", + "symfony/expression-language": "^6.4|^7.0", + "vimeo/psalm": "^6.0" }, "suggest": { "composer/semver": "For using version-related requirements", - "ocramius/package-versions": "For using the 'package' requirement", + "composer/package-versions-deprecated": "For using the 'package' requirement", "symfony/expression-language": "For using expression-based requirements and/or expectations" }, "autoload": { @@ -42,8 +41,6 @@ "*": "dist" }, "sort-packages": true, - "allow-plugins": { - "ocramius/package-versions": true - } + "allow-plugins": {} } } diff --git a/phpunit-extension.xml b/phpunit-extension.xml index a9c0e1b..bce6eae 100644 --- a/phpunit-extension.xml +++ b/phpunit-extension.xml @@ -1,9 +1,8 @@ @@ -19,13 +18,13 @@ - - + + src - - + + - + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index da7a232..031be84 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,9 +1,8 @@ @@ -20,9 +19,9 @@ - - + + src - - + + diff --git a/psalm.xml b/psalm.xml index 9c44c00..7295bed 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,18 +14,20 @@ - - - - - - - - - + + + + + + + + + + + diff --git a/src/Annotation/AnnotationExtension.php b/src/Annotation/AnnotationExtension.php index 7d65af4..93668d5 100644 --- a/src/Annotation/AnnotationExtension.php +++ b/src/Annotation/AnnotationExtension.php @@ -13,16 +13,20 @@ namespace PHPUnitExtras\Annotation; -use PHPUnit\Runner\BeforeTestHook; +use PHPUnit\Runner\Extension\Extension; +use PHPUnit\Runner\Extension\Facade; +use PHPUnit\Runner\Extension\ParameterCollection; +use PHPUnit\TextUI\Configuration\Configuration; -class AnnotationExtension implements BeforeTestHook +class AnnotationExtension implements Extension { use Annotations; - public function executeBeforeTest(string $test) : void + private ?AnnotationProcessorBuilder $processorBuilder = null; + + #[\Override] + public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters) : void { - /** @var class-string $class */ - [$class, $method] = preg_split('/ |::/', $test); - $this->processAnnotations($class, $method); + $facade->registerSubscriber(new AnnotationSubscriber($this)); } } diff --git a/src/Annotation/AnnotationProcessor.php b/src/Annotation/AnnotationProcessor.php index 8d97469..ec9b76f 100644 --- a/src/Annotation/AnnotationProcessor.php +++ b/src/Annotation/AnnotationProcessor.php @@ -17,8 +17,8 @@ final class AnnotationProcessor { - private $processorMap; - private $placeholderResolver; + private ProcessorMap $processorMap; + private PlaceholderResolver $placeholderResolver; public function __construct(ProcessorMap $processorMap, PlaceholderResolver $placeholderResolver) { diff --git a/src/Annotation/AnnotationProcessorBuilder.php b/src/Annotation/AnnotationProcessorBuilder.php index 1c13682..a90ee89 100644 --- a/src/Annotation/AnnotationProcessorBuilder.php +++ b/src/Annotation/AnnotationProcessorBuilder.php @@ -36,16 +36,9 @@ final class AnnotationProcessorBuilder /** @var array */ private $placeholderResolvers = []; - /** @var array */ - private $ignoredAnnotations = []; - - /** @var bool */ - private $ignoreUnknownAnnotations = false; - public static function fromDefaults() : self { return (new self()) - ->ignoreEstablishedAnnotations() ->addRequirement(ConditionRequirement::fromGlobals()) ->addRequirement(new ConstantRequirement()) ->addRequirement(new PackageRequirement()) @@ -80,27 +73,6 @@ public function addPlaceholderResolver(PlaceholderResolver $resolver) : self return $this; } - public function ignoreUnknownAnnotations(bool $ignore = true) : self - { - $this->ignoreUnknownAnnotations = $ignore; - - return $this; - } - - public function ignoreAnnotation(string $name) : self - { - $this->ignoredAnnotations[$name] = true; - - return $this; - } - - public function ignoreEstablishedAnnotations() : self - { - $this->ignoredAnnotations = EstablishedAnnotationNames::ALL + $this->ignoredAnnotations; - - return $this; - } - public function build() : AnnotationProcessor { $processors = $this->processors; @@ -111,7 +83,7 @@ public function build() : AnnotationProcessor } return new AnnotationProcessor( - new ProcessorMap($processors, array_keys($this->ignoredAnnotations), $this->ignoreUnknownAnnotations), + new ProcessorMap($processors), new ChainResolver($this->placeholderResolvers) ); } diff --git a/src/Annotation/AnnotationSubscriber.php b/src/Annotation/AnnotationSubscriber.php new file mode 100644 index 0000000..3a140dd --- /dev/null +++ b/src/Annotation/AnnotationSubscriber.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PHPUnitExtras\Annotation; + +use PHPUnit\Event\Test\PreparationStarted; +use PHPUnit\Event\Test\PreparationStartedSubscriber; + +final class AnnotationSubscriber implements PreparationStartedSubscriber +{ + private AnnotationExtension $extension; + + public function __construct(AnnotationExtension $extension) + { + $this->extension = $extension; + } + + #[\Override] + public function notify(PreparationStarted $event) : void + { + $test = $event->test(); + + if (!method_exists($test, 'className') || !method_exists($test, 'methodName')) { + return; + } + + /** @var class-string $class */ + $class = $test->className(); + + $this->extension->processTestAttributes($class, $test->methodName()); + } +} diff --git a/src/Annotation/Annotations.php b/src/Annotation/Annotations.php index c4f7cd0..244bfec 100644 --- a/src/Annotation/Annotations.php +++ b/src/Annotation/Annotations.php @@ -13,15 +13,14 @@ namespace PHPUnitExtras\Annotation; -use PHPUnit\Util\Test; +use PHPUnitExtras\Annotation\Attribute\AnnotationAttribute; trait Annotations { - /** @var AnnotationProcessor|null */ - private $annotationProcessor; + private ?AnnotationProcessor $annotationProcessor = null; /** @var array */ - private static $processedClasses = []; + private array $processedClasses = []; protected function createAnnotationProcessorBuilder() : AnnotationProcessorBuilder { @@ -31,21 +30,38 @@ protected function createAnnotationProcessorBuilder() : AnnotationProcessorBuild /** * @param class-string $class */ - private function processAnnotations(string $class, string $method) : void + final public function processTestAttributes(string $class, string $method) : void { - $annotations = Test::parseTestMethodAnnotations($class, $method); + $classAttributes = $this->collectAttributes(new \ReflectionClass($class)); - if ($annotations['class'] && !isset(self::$processedClasses[$class])) { - $this->getAnnotationProcessor()->process($annotations['class'], new Target($class)); - self::$processedClasses[$class] = true; + if ($classAttributes && !isset($this->processedClasses[$class])) { + $this->getAnnotationProcessor()->process($classAttributes, new Target($class)); + $this->processedClasses[$class] = true; } - if ($annotations['method']) { - $this->getAnnotationProcessor()->process($annotations['method'], new Target($class, $method)); + $methodAttributes = $this->collectAttributes(new \ReflectionMethod($class, $method)); + if ($methodAttributes) { + $this->getAnnotationProcessor()->process($methodAttributes, new Target($class, $method)); } } - private function getAnnotationProcessor() : AnnotationProcessor + /** + * @return array> + */ + private function collectAttributes(\ReflectionClass|\ReflectionMethod $reflector) : array + { + $annotations = []; + foreach ($reflector->getAttributes(AnnotationAttribute::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $instance = $attribute->newInstance(); + \assert($instance instanceof AnnotationAttribute); + + $annotations[$instance->getName()][] = $instance->getValue(); + } + + return $annotations; + } + + final protected function getAnnotationProcessor() : AnnotationProcessor { if ($this->annotationProcessor) { return $this->annotationProcessor; diff --git a/src/Annotation/Attribute/AnnotationAttribute.php b/src/Annotation/Attribute/AnnotationAttribute.php new file mode 100644 index 0000000..f8e614d --- /dev/null +++ b/src/Annotation/Attribute/AnnotationAttribute.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PHPUnitExtras\Annotation\Attribute; + +interface AnnotationAttribute +{ + public function getName() : string; + + public function getValue() : string; +} diff --git a/src/Annotation/Attribute/Requires.php b/src/Annotation/Attribute/Requires.php new file mode 100644 index 0000000..db268b3 --- /dev/null +++ b/src/Annotation/Attribute/Requires.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PHPUnitExtras\Annotation\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Requires implements AnnotationAttribute +{ + private string $type; + private string $value; + + public function __construct(string $type, string $value = '') + { + $this->type = $type; + $this->value = $value; + } + + #[\Override] + public function getName() : string + { + return 'requires'; + } + + #[\Override] + public function getValue() : string + { + return trim("$this->type $this->value"); + } +} diff --git a/src/Annotation/EstablishedAnnotationNames.php b/src/Annotation/EstablishedAnnotationNames.php deleted file mode 100644 index 72bfd81..0000000 --- a/src/Annotation/EstablishedAnnotationNames.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace PHPUnitExtras\Annotation; - -final class EstablishedAnnotationNames -{ - public const PHPUNIT = [ - 'author' => true, - 'after' => true, - 'afterClass' => true, - 'backupGlobals' => true, - 'backupStaticAttributes' => true, - 'before' => true, - 'beforeClass' => true, - 'codeCoverageIgnore' => true, - 'codeCoverageIgnoreStart' => true, - 'codeCoverageIgnoreEnd' => true, - 'covers' => true, - 'coversDefaultClass' => true, - 'coversNothing' => true, - 'dataProvider' => true, - 'depends' => true, - 'doesNotPerformAssertions' => true, - 'expectedException' => true, - 'expectedExceptionCode' => true, - 'expectedExceptionMessage' => true, - 'expectedExceptionMessageRegExp' => true, - 'group' => true, - 'large' => true, - 'medium' => true, - 'preserveGlobalState' => true, - 'preCondition' => true, - 'postCondition' => true, - 'requires' => true, - 'runTestsInSeparateProcesses' => true, - 'runInSeparateProcess' => true, - 'small' => true, - 'test' => true, - 'testdox' => true, - 'testWith' => true, - 'ticket' => true, - 'uses' => true, - ]; - - public const MISC = [ - 'fixme' => true, - 'FIXME' => true, - 'todo' => true, - 'TODO' => true, - 'param' => true, - 'return' => true, - 'see' => true, - 'throws' => true, - ]; - - public const ALL = self::PHPUNIT + self::MISC; - - private function __construct() - { - } -} diff --git a/src/Annotation/InvalidAnnotationException.php b/src/Annotation/InvalidAnnotationException.php index 117f31d..e2053bd 100644 --- a/src/Annotation/InvalidAnnotationException.php +++ b/src/Annotation/InvalidAnnotationException.php @@ -13,28 +13,25 @@ namespace PHPUnitExtras\Annotation; -use PHPUnit\Framework\Exception; - -/** @psalm-suppress InternalMethod */ -final class InvalidAnnotationException extends Exception +final class InvalidAnnotationException extends \RuntimeException { public static function unknownName(string $name) : self { - return new self(sprintf('Unknown annotation "%s"', $name)); + return new self(\sprintf('Unknown annotation "%s"', $name)); } public static function invalidSyntax(string $annotation, string $reason = '') : self { - return new self(sprintf('Unable to parse "%s": %s', $annotation, $reason)); + return new self(\sprintf('Unable to parse "%s": %s', $annotation, $reason)); } public static function unresolvedPlaceholder(string $placeholder) : self { - return new self(sprintf('Unresolved placeholder "%s"', $placeholder)); + return new self(\sprintf('Unresolved placeholder "%s"', $placeholder)); } public static function unknownRequirement(string $requirement) : self { - return new self(sprintf('Unknown requirement "%s"', $requirement)); + return new self(\sprintf('Unknown requirement "%s"', $requirement)); } } diff --git a/src/Annotation/PlaceholderResolver/ChainResolver.php b/src/Annotation/PlaceholderResolver/ChainResolver.php index 9de2404..1aeb590 100644 --- a/src/Annotation/PlaceholderResolver/ChainResolver.php +++ b/src/Annotation/PlaceholderResolver/ChainResolver.php @@ -35,11 +35,13 @@ public function addResolver(PlaceholderResolver $resolver) : self return $this; } + #[\Override] public function getName() : string { return 'chain'; } + #[\Override] public function resolve(string $value, Target $target) : string { foreach ($this->resolvers as $resolver) { diff --git a/src/Annotation/PlaceholderResolver/TargetClassResolver.php b/src/Annotation/PlaceholderResolver/TargetClassResolver.php index 482c13d..3f04f89 100644 --- a/src/Annotation/PlaceholderResolver/TargetClassResolver.php +++ b/src/Annotation/PlaceholderResolver/TargetClassResolver.php @@ -17,11 +17,13 @@ final class TargetClassResolver implements PlaceholderResolver { + #[\Override] public function getName() : string { return 'target_class'; } + #[\Override] public function resolve(string $value, Target $target) : string { return strtr($value, [ diff --git a/src/Annotation/PlaceholderResolver/TargetMethodResolver.php b/src/Annotation/PlaceholderResolver/TargetMethodResolver.php index d7f08cc..8105a9b 100644 --- a/src/Annotation/PlaceholderResolver/TargetMethodResolver.php +++ b/src/Annotation/PlaceholderResolver/TargetMethodResolver.php @@ -17,11 +17,13 @@ final class TargetMethodResolver implements PlaceholderResolver { + #[\Override] public function getName() : string { return 'target_method'; } + #[\Override] public function resolve(string $value, Target $target) : string { if (!$target->isOnMethod()) { diff --git a/src/Annotation/PlaceholderResolver/TmpDirResolver.php b/src/Annotation/PlaceholderResolver/TmpDirResolver.php index 70e7025..d850349 100644 --- a/src/Annotation/PlaceholderResolver/TmpDirResolver.php +++ b/src/Annotation/PlaceholderResolver/TmpDirResolver.php @@ -17,11 +17,13 @@ final class TmpDirResolver implements PlaceholderResolver { + #[\Override] public function getName() : string { return 'tmp_dir'; } + #[\Override] public function resolve(string $value, Target $target) : string { return strtr($value, ['%tmp_dir%' => sys_get_temp_dir()]); diff --git a/src/Annotation/Processor/RequiresProcessor.php b/src/Annotation/Processor/RequiresProcessor.php index 2b04a5d..a369c1d 100644 --- a/src/Annotation/Processor/RequiresProcessor.php +++ b/src/Annotation/Processor/RequiresProcessor.php @@ -53,11 +53,13 @@ public function getRequirements() : array return $this->requirements; } + #[\Override] public function getName() : string { return 'requires'; } + #[\Override] public function process(string $value) : void { [$reqName, $reqValue] = explode(' ', $value, 2) + [1 => '']; diff --git a/src/Annotation/ProcessorMap.php b/src/Annotation/ProcessorMap.php index 7adae88..fa5d715 100644 --- a/src/Annotation/ProcessorMap.php +++ b/src/Annotation/ProcessorMap.php @@ -20,24 +20,14 @@ final class ProcessorMap /** @var array */ private $processors = []; - /** @var array */ - private $ignoredAnnotationNames; - - /** @var bool */ - private $ignoreUnknownAnnotations; - /** * @param array $processors - * @param array $ignoredAnnotationNames */ - public function __construct(array $processors, array $ignoredAnnotationNames = [], bool $ignoreUnknownAnnotations = false) + public function __construct(array $processors) { foreach ($processors as $processor) { $this->addProcessor($processor); } - - $this->ignoredAnnotationNames = array_fill_keys($ignoredAnnotationNames, true); - $this->ignoreUnknownAnnotations = $ignoreUnknownAnnotations; } public function get(string $name) : Processor @@ -55,14 +45,6 @@ public function tryGet(string $name) : ?Processor return $this->processors[$name]; } - if (isset($this->ignoredAnnotationNames[$name])) { - return null; - } - - if ($this->ignoreUnknownAnnotations) { - return null; - } - throw InvalidAnnotationException::unknownName($name); } diff --git a/src/Annotation/Requirement/ConditionFunctionProvider.php b/src/Annotation/Requirement/ConditionFunctionProvider.php index 33825ff..6a4140b 100644 --- a/src/Annotation/Requirement/ConditionFunctionProvider.php +++ b/src/Annotation/Requirement/ConditionFunctionProvider.php @@ -18,6 +18,7 @@ final class ConditionFunctionProvider implements ExpressionFunctionProviderInterface { + #[\Override] public function getFunctions() : array { return [ diff --git a/src/Annotation/Requirement/ConditionRequirement.php b/src/Annotation/Requirement/ConditionRequirement.php index 604f4a2..5439102 100644 --- a/src/Annotation/Requirement/ConditionRequirement.php +++ b/src/Annotation/Requirement/ConditionRequirement.php @@ -42,26 +42,32 @@ public static function fromGlobals() : self ]); } + #[\Override] public function getName() : string { return 'condition'; } + #[\Override] public function check(string $value) : ?string { if ($this->language->evaluate($value, $this->context)) { return null; } - return sprintf('"%s" is not evaluated to true', $value); + return \sprintf('"%s" is not evaluated to true', $value); } /** * A workaround for unsupported "nullsafe" and "null coalescing" operators. * @see https://github.com/symfony/symfony/issues/21691 + * + * @param array $data + * @return \ArrayObject */ private static function wrapGlobal(array $data) : \ArrayObject { + /** @psalm-suppress MissingTemplateParam */ return new class($data) extends \ArrayObject { public function __get($key) { diff --git a/src/Annotation/Requirement/ConstantRequirement.php b/src/Annotation/Requirement/ConstantRequirement.php index 2ae3343..5157301 100644 --- a/src/Annotation/Requirement/ConstantRequirement.php +++ b/src/Annotation/Requirement/ConstantRequirement.php @@ -15,17 +15,19 @@ final class ConstantRequirement implements Requirement { + #[\Override] public function getName() : string { return 'constant'; } + #[\Override] public function check(string $value) : ?string { if (\defined($value)) { return null; } - return sprintf('The constant "%s" is undefined', $value); + return \sprintf('The constant "%s" is undefined', $value); } } diff --git a/src/Annotation/Requirement/PackageRequirement.php b/src/Annotation/Requirement/PackageRequirement.php index d11ff2d..134dea9 100644 --- a/src/Annotation/Requirement/PackageRequirement.php +++ b/src/Annotation/Requirement/PackageRequirement.php @@ -18,15 +18,16 @@ final class PackageRequirement implements Requirement { + #[\Override] public function getName() : string { return 'package'; } + #[\Override] public function check(string $value) : ?string { /** - * @var string $packageName * @see https://github.com/vimeo/psalm/issues/3118 */ [$packageName, $versionConstraints] = explode(' ', $value, 2) + [1 => null]; @@ -34,7 +35,7 @@ public function check(string $value) : ?string try { $packageVersion = Versions::getVersion($packageName); } catch (\OutOfBoundsException $e) { - return sprintf('Package "%s" is required', $value); + return \sprintf('Package "%s" is required', $value); } if (!$versionConstraints) { @@ -46,6 +47,6 @@ public function check(string $value) : ?string return null; } - return sprintf('"%s" version %s is required', $packageName, $versionConstraints); + return \sprintf('"%s" version %s is required', $packageName, $versionConstraints); } } diff --git a/src/Annotation/Target.php b/src/Annotation/Target.php index 17ea95d..965c52a 100644 --- a/src/Annotation/Target.php +++ b/src/Annotation/Target.php @@ -17,8 +17,8 @@ final class Target { - private $className; - private $methodName; + private string $className; + private ?string $methodName; /** * @param class-string $className @@ -31,7 +31,7 @@ public function __construct(string $className, ?string $methodName = null) public static function fromTestCase(TestCase $testCase) : self { - return new self(\get_class($testCase), $testCase->getName(false)); + return new self($testCase::class, $testCase->name()); } public function getClassName() : string @@ -52,7 +52,7 @@ public function isOnMethod() : bool public function getMethodName() : string { if (null === $this->methodName) { - throw new \LogicException(sprintf('Class level target "%s" does not have method name', $this->className)); + throw new \LogicException(\sprintf('Class level target "%s" does not have method name', $this->className)); } return $this->methodName; @@ -62,7 +62,7 @@ public function getMethodShortName() : string { $methodName = $this->getMethodName(); - return 0 === strpos($methodName, 'test') + return str_starts_with($methodName, 'test') ? substr($methodName, 4) : $methodName; } diff --git a/src/Expectation/ChainExpectation.php b/src/Expectation/ChainExpectation.php index 60a9026..e12cbdf 100644 --- a/src/Expectation/ChainExpectation.php +++ b/src/Expectation/ChainExpectation.php @@ -25,6 +25,7 @@ public function expect(Expectation $expectation) : void $this->expectations[] = $expectation; } + #[\Override] public function verify() : void { try { diff --git a/src/Expectation/Expectations.php b/src/Expectation/Expectations.php index 5d76961..c74fc0b 100644 --- a/src/Expectation/Expectations.php +++ b/src/Expectation/Expectations.php @@ -15,8 +15,7 @@ trait Expectations { - /** @var ChainExpectation|null */ - private $expectations; + private ?ChainExpectation $expectations = null; final protected function verifyExpectations() : void { diff --git a/src/Expectation/ExpressionExpectation.php b/src/Expectation/ExpressionExpectation.php index 95c71fc..25f7f6b 100644 --- a/src/Expectation/ExpressionExpectation.php +++ b/src/Expectation/ExpressionExpectation.php @@ -30,6 +30,7 @@ public function __construct(ExpressionContext $context, ?ExpressionLanguage $lan $this->language = $language ?: new ExpressionLanguage(); } + #[\Override] public function verify() : void { $expression = $this->context->getExpression(); diff --git a/src/Expectation/IsTruthyExpression.php b/src/Expectation/IsTruthyExpression.php index 133dff7..9e7e4bf 100644 --- a/src/Expectation/IsTruthyExpression.php +++ b/src/Expectation/IsTruthyExpression.php @@ -14,49 +14,36 @@ namespace PHPUnitExtras\Expectation; use PHPUnit\Framework\Constraint\Constraint; -use SebastianBergmann\Exporter\Exporter; final class IsTruthyExpression extends Constraint { - private $context; - - /** @var Exporter|null */ - private $compatExporter; + private ExpressionContext $context; public function __construct(ExpressionContext $context) { $this->context = $context; } + #[\Override] public function toString() : string { return 'is evaluated to true'; } + #[\Override] protected function matches($other) : bool { return true === $other; } + #[\Override] protected function failureDescription($other) : string { - return sprintf( + return \sprintf( "\"%s\" with values\n %s\n%s", $this->context->getExpression(), $this->exporter()->export($this->context->getValues(), 1), $this->toString() ); } - - /** - * Needed for backward compatibility with PHPUnit 7. - */ - protected function exporter() : Exporter - { - if (null === $this->compatExporter) { - $this->compatExporter = new Exporter(); - } - - return $this->compatExporter; - } } diff --git a/src/TestCase.php b/src/TestCase.php index 9965533..08039b2 100644 --- a/src/TestCase.php +++ b/src/TestCase.php @@ -13,6 +13,8 @@ namespace PHPUnitExtras; +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\TestCase as BaseTestCase; use PHPUnitExtras\Annotation\Annotations; use PHPUnitExtras\Annotation\Target; @@ -23,18 +25,10 @@ abstract class TestCase extends BaseTestCase use Annotations; use Expectations; - /** - * @before - */ + #[Before] final protected function processTestCaseAnnotations() : void { - /** - * @psalm-suppress TypeDoesNotContainType - * @psalm-suppress TypeDoesNotContainNull - * @psalm-suppress RedundantCondition - * TestCase::getName() may return null on PHPUnit 7 - */ - $this->processAnnotations(static::class, $this->getName(false) ?? ''); + $this->processTestAttributes(static::class, $this->name()); } final protected function resolvePlaceholders(string $value) : string @@ -44,9 +38,7 @@ final protected function resolvePlaceholders(string $value) : string return $resolver->resolve($value, Target::fromTestCase($this)); } - /** - * @after - */ + #[After] final protected function verifyTestCaseExpectations() : void { $this->verifyExpectations(); diff --git a/tests/Annotation/AnnotationExtensionTest.php b/tests/Annotation/AnnotationExtensionTest.php index 73434be..c2edcf1 100755 --- a/tests/Annotation/AnnotationExtensionTest.php +++ b/tests/Annotation/AnnotationExtensionTest.php @@ -15,10 +15,8 @@ use PHPUnit\Framework\TestCase; -/** - * @log %tmp_dir%/%target_class%.log,class annotation 1 - * @log %tmp_dir%/%target_class%.log,class annotation 2 - */ +#[Log('%tmp_dir%/%target_class%.log,class annotation 1')] +#[Log('%tmp_dir%/%target_class%.log,class annotation 2')] final class AnnotationExtensionTest extends TestCase { public static function setUpBeforeClass() : void @@ -26,10 +24,8 @@ public static function setUpBeforeClass() : void @unlink(self::getLogFilename()); } - /** - * @log %tmp_dir%/%target_class%.log,method annotation 1 - * @log %tmp_dir%/%target_class%.log,method annotation 2 - */ + #[Log('%tmp_dir%/%target_class%.log,method annotation 1')] + #[Log('%tmp_dir%/%target_class%.log,method annotation 2')] public function testAllAnnotationsAreProcessed() : void { $filename = self::getLogFilename(); @@ -48,7 +44,7 @@ public function testAllAnnotationsAreProcessed() : void private static function getLogFilename() : string { - return sprintf('%s/%s.log', + return \sprintf('%s/%s.log', sys_get_temp_dir(), (new \ReflectionClass(__CLASS__))->getShortName() ); diff --git a/tests/Annotation/AnnotationProcessorTest.php b/tests/Annotation/AnnotationProcessorTest.php index 2d107d5..cef06b9 100755 --- a/tests/Annotation/AnnotationProcessorTest.php +++ b/tests/Annotation/AnnotationProcessorTest.php @@ -55,35 +55,4 @@ public function testProcessThrowsExceptionOnUnknownAnnotation() : void $this->expectExceptionMessage('Unknown annotation "bar"'); $processor->process($annotations, new Target('fooClass')); } - - public function testProcessIgnoresUnknownAnnotation() : void - { - $processorMap = new ProcessorMap([$foo = new MockProcessor('foo')], [], true); - $processor = new AnnotationProcessor($processorMap, new ChainResolver()); - - $annotations = [ - 'foo' => ['foo_value'], - 'bar' => ['bar_value'], - ]; - - $processor->process($annotations, Target::fromTestCase($this)); - - self::assertSame($foo->lastProcessedValue, $annotations['foo'][0]); - } - - public function testProcessIgnoresIgnoredAnnotations() : void - { - $processorMap = new ProcessorMap([$bar = new MockProcessor('bar')], ['foo', 'baz']); - $processor = new AnnotationProcessor($processorMap, new ChainResolver()); - - $annotations = [ - 'foo' => ['foo_value'], - 'bar' => ['bar_value'], - 'baz' => ['baz_value'], - ]; - - $processor->process($annotations, new Target('fooClass')); - - self::assertSame($bar->lastProcessedValue, $annotations['bar'][0]); - } } diff --git a/tests/Annotation/AnnotationTestCaseTest.php b/tests/Annotation/AnnotationTestCaseTest.php index c1c8bf5..2b5820e 100755 --- a/tests/Annotation/AnnotationTestCaseTest.php +++ b/tests/Annotation/AnnotationTestCaseTest.php @@ -16,10 +16,8 @@ use PHPUnitExtras\Annotation\AnnotationProcessorBuilder; use PHPUnitExtras\TestCase; -/** - * @log %tmp_dir%/%target_class%.log,class annotation 1 - * @log %tmp_dir%/%target_class%.log,class annotation 2 - */ +#[Log('%tmp_dir%/%target_class%.log,class annotation 1')] +#[Log('%tmp_dir%/%target_class%.log,class annotation 2')] final class AnnotationTestCaseTest extends TestCase { public static function setUpBeforeClass() : void @@ -33,10 +31,8 @@ protected function createAnnotationProcessorBuilder() : AnnotationProcessorBuild ->addProcessor(new LogProcessor()); } - /** - * @log %tmp_dir%/%target_class%.log,method annotation 1 - * @log %tmp_dir%/%target_class%.log,method annotation 2 - */ + #[Log('%tmp_dir%/%target_class%.log,method annotation 1')] + #[Log('%tmp_dir%/%target_class%.log,method annotation 2')] public function testAllAnnotationsAreProcessed() : void { $filename = self::getLogFilename(); @@ -63,7 +59,7 @@ public function testResolvePlaceholdersSubstitutesAllPlaceholders() : void private static function getLogFilename() : string { - return sprintf('%s/%s.log', + return \sprintf('%s/%s.log', sys_get_temp_dir(), (new \ReflectionClass(__CLASS__))->getShortName() ); diff --git a/tests/Annotation/Attribute/RequiresTest.php b/tests/Annotation/Attribute/RequiresTest.php new file mode 100644 index 0000000..00b447e --- /dev/null +++ b/tests/Annotation/Attribute/RequiresTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PHPUnitExtras\Tests\Annotation\Attribute; + +use PHPUnit\Framework\TestCase; +use PHPUnitExtras\Annotation\Attribute\Requires; + +final class RequiresTest extends TestCase +{ + public function testGetNameReturnsRequiresProcessorName() : void + { + $attribute = new Requires('condition', 'server.FOO'); + + self::assertSame('requires', $attribute->getName()); + } + + public function testGetValuePreservesRequirementTypeAndValue() : void + { + $attribute = new Requires('condition', 'server.FOO'); + + self::assertSame('condition server.FOO', $attribute->getValue()); + } + + public function testGetValueSupportsRequirementWithoutValue() : void + { + $attribute = new Requires('extension'); + + self::assertSame('extension', $attribute->getValue()); + } +} diff --git a/tests/Annotation/Log.php b/tests/Annotation/Log.php new file mode 100644 index 0000000..7dc89e8 --- /dev/null +++ b/tests/Annotation/Log.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace PHPUnitExtras\Tests\Annotation; + +use PHPUnitExtras\Annotation\Attribute\AnnotationAttribute; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +final class Log implements AnnotationAttribute +{ + private string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public function getName() : string + { + return 'log'; + } + + public function getValue() : string + { + return $this->value; + } +} diff --git a/tests/Annotation/PlaceholderResolver/TargetClassResolverTest.php b/tests/Annotation/PlaceholderResolver/TargetClassResolverTest.php index db514a9..d190829 100755 --- a/tests/Annotation/PlaceholderResolver/TargetClassResolverTest.php +++ b/tests/Annotation/PlaceholderResolver/TargetClassResolverTest.php @@ -13,15 +13,14 @@ namespace PHPUnitExtras\Tests\Annotation\PlaceholderResolver; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PHPUnitExtras\Annotation\PlaceholderResolver\TargetClassResolver; use PHPUnitExtras\Annotation\Target; final class TargetClassResolverTest extends TestCase { - /** - * @dataProvider provideResolveSubstitutesSupportedPlaceholdersData() - */ + #[DataProvider('provideResolveSubstitutesSupportedPlaceholdersData')] public function testResolveSubstitutesSupportedPlaceholders(string $value, Target $target, $expectedResult) : void { $resolver = new TargetClassResolver(); @@ -29,12 +28,12 @@ public function testResolveSubstitutesSupportedPlaceholders(string $value, Targe self::assertSame($expectedResult, $resolver->resolve($value, $target)); } - public function provideResolveSubstitutesSupportedPlaceholdersData() : iterable + public static function provideResolveSubstitutesSupportedPlaceholdersData() : iterable { $classTarget = new Target(__CLASS__); $methodTarget = new Target(__CLASS__, __METHOD__); - $resolvedValue = sprintf('[%s]', (new \ReflectionClass(__CLASS__))->getShortName()); - $resolvedFullValue = sprintf('[%s]', __CLASS__); + $resolvedValue = \sprintf('[%s]', (new \ReflectionClass(__CLASS__))->getShortName()); + $resolvedFullValue = \sprintf('[%s]', __CLASS__); return [ ['[%target_class%]', $classTarget, $resolvedValue], diff --git a/tests/Annotation/PlaceholderResolver/TargetMethodResolverTest.php b/tests/Annotation/PlaceholderResolver/TargetMethodResolverTest.php index 63666ef..3a8277b 100755 --- a/tests/Annotation/PlaceholderResolver/TargetMethodResolverTest.php +++ b/tests/Annotation/PlaceholderResolver/TargetMethodResolverTest.php @@ -13,15 +13,14 @@ namespace PHPUnitExtras\Tests\Annotation\PlaceholderResolver; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PHPUnitExtras\Annotation\PlaceholderResolver\TargetMethodResolver; use PHPUnitExtras\Annotation\Target; final class TargetMethodResolverTest extends TestCase { - /** - * @dataProvider provideResolveSubstitutesSupportedPlaceholdersData() - */ + #[DataProvider('provideResolveSubstitutesSupportedPlaceholdersData')] public function testResolveSubstitutesSupportedPlaceholders(string $value, Target $target, $expectedResult) : void { $resolver = new TargetMethodResolver(); @@ -29,12 +28,12 @@ public function testResolveSubstitutesSupportedPlaceholders(string $value, Targe self::assertSame($expectedResult, $resolver->resolve($value, $target)); } - public function provideResolveSubstitutesSupportedPlaceholdersData() : iterable + public static function provideResolveSubstitutesSupportedPlaceholdersData() : iterable { $classTarget = new Target(__CLASS__); $methodTarget = new Target(__CLASS__, 'testResolveSubstitutesPlaceholders'); - $resolvedValue = sprintf('[%s]', 'ResolveSubstitutesPlaceholders'); - $resolvedFullValue = sprintf('[%s]', 'testResolveSubstitutesPlaceholders'); + $resolvedValue = \sprintf('[%s]', 'ResolveSubstitutesPlaceholders'); + $resolvedFullValue = \sprintf('[%s]', 'testResolveSubstitutesPlaceholders'); return [ ['[%target_method%]', $classTarget, '[%target_method%]'], diff --git a/tests/Annotation/PlaceholderResolver/TmpDirResolverTest.php b/tests/Annotation/PlaceholderResolver/TmpDirResolverTest.php index 1b6ac90..4e2cce9 100755 --- a/tests/Annotation/PlaceholderResolver/TmpDirResolverTest.php +++ b/tests/Annotation/PlaceholderResolver/TmpDirResolverTest.php @@ -13,15 +13,14 @@ namespace PHPUnitExtras\Tests\Annotation\PlaceholderResolver; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PHPUnitExtras\Annotation\PlaceholderResolver\TmpDirResolver; use PHPUnitExtras\Annotation\Target; final class TmpDirResolverTest extends TestCase { - /** - * @dataProvider provideResolveSubstitutesSupportedPlaceholderData - */ + #[DataProvider('provideResolveSubstitutesSupportedPlaceholderData')] public function testResolveSubstitutesSupportedPlaceholder(string $value, $expectedResult) : void { $resolver = new TmpDirResolver(); @@ -29,7 +28,7 @@ public function testResolveSubstitutesSupportedPlaceholder(string $value, $expec self::assertSame($expectedResult, $resolver->resolve($value, new Target('fooClass'))); } - public function provideResolveSubstitutesSupportedPlaceholderData() : iterable + public static function provideResolveSubstitutesSupportedPlaceholderData() : iterable { return [ ['[%tmp_dir%]', '['.sys_get_temp_dir().']'], diff --git a/tests/Annotation/Processor/RequiresProcessorTest.php b/tests/Annotation/Processor/RequiresProcessorTest.php index 4a11cb6..9e52ce8 100755 --- a/tests/Annotation/Processor/RequiresProcessorTest.php +++ b/tests/Annotation/Processor/RequiresProcessorTest.php @@ -13,6 +13,8 @@ namespace PHPUnitExtras\Tests\Annotation\Processor; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; use PHPUnit\Framework\TestCase; use PHPUnitExtras\Annotation\InvalidAnnotationException; use PHPUnitExtras\Annotation\Processor\RequiresProcessor; @@ -52,10 +54,8 @@ public function testProcessFailsOnUnknownRequirement() : void $processor->process('Bazqux 42'); } - /** - * @doesNotPerformAssertions - * @dataProvider provideProcessSkipsPhpUnitRequirementsData - */ + #[DoesNotPerformAssertions] + #[DataProvider('provideProcessSkipsPhpUnitRequirementsData')] public function testProcessSkipsPhpUnitRequirements(string $phpunitRequirement) : void { $requirement = $this->createMock(Requirement::class); @@ -65,7 +65,7 @@ public function testProcessSkipsPhpUnitRequirements(string $phpunitRequirement) $processor->process($phpunitRequirement); } - public function provideProcessSkipsPhpUnitRequirementsData() : iterable + public static function provideProcessSkipsPhpUnitRequirementsData() : iterable { return [ ['PHP 8.0'], diff --git a/tests/Annotation/Requirement/ConditionRequirementTest.php b/tests/Annotation/Requirement/ConditionRequirementTest.php index 9789e07..c1ad936 100755 --- a/tests/Annotation/Requirement/ConditionRequirementTest.php +++ b/tests/Annotation/Requirement/ConditionRequirementTest.php @@ -13,6 +13,7 @@ namespace PHPUnitExtras\Tests\Annotation\Requirement; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use PHPUnitExtras\Annotation\Requirement\ConditionRequirement; @@ -47,9 +48,7 @@ public function testCheckFailsForFalsyExpressionUsingGlobalContext() : void self::assertSame("\"$expr\" is not evaluated to true", $requirement->check($expr)); } - /** - * @dataProvider provideSupportedGlobals - */ + #[DataProvider('provideSupportedGlobals')] public function testCheckEvaluatesMissingKeyInGlobalContextToNull(string $globalName) : void { $requirement = ConditionRequirement::fromGlobals(); @@ -58,7 +57,7 @@ public function testCheckEvaluatesMissingKeyInGlobalContextToNull(string $global self::assertSame("\"$expr\" is not evaluated to true", $requirement->check($expr)); } - public function provideSupportedGlobals() : iterable + public static function provideSupportedGlobals() : iterable { return [ ['cookie'], diff --git a/tests/Expectation/ExpressionExpectationTest.php b/tests/Expectation/ExpressionExpectationTest.php index 3329823..afe5b8d 100755 --- a/tests/Expectation/ExpressionExpectationTest.php +++ b/tests/Expectation/ExpressionExpectationTest.php @@ -18,12 +18,9 @@ use PHPUnit\Framework\TestCase; use PHPUnitExtras\Expectation\ExpressionContext; use PHPUnitExtras\Expectation\ExpressionExpectation; -use PHPUnitExtras\Tests\PHPUnitCompat; final class ExpressionExpectationTest extends TestCase { - use PHPUnitCompat; - /** @var ExpressionContext|MockObject */ private $context; diff --git a/tests/PHPUnitCompat.php b/tests/PHPUnitCompat.php deleted file mode 100644 index d4ffe98..0000000 --- a/tests/PHPUnitCompat.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace PHPUnitExtras\Tests; - -/** - * A compatibility layer for the legacy PHPUnit 7. - */ -trait PHPUnitCompat -{ - public function expectExceptionMessageMatches(string $regularExpression) : void - { - \is_callable('parent::expectExceptionMessageMatches') - ? parent::expectExceptionMessageMatches($regularExpression) - : parent::expectExceptionMessageRegExp($regularExpression); - } -}