From dd3c04927ca8ee8a985bfc441338620b6d549035 Mon Sep 17 00:00:00 2001 From: Dmitrii Gridnev Date: Thu, 9 Apr 2026 11:43:35 +0300 Subject: [PATCH] feat: add Tags attribute support Add #[Tags('smoke', 'regression')] attribute for test-level tagging. Tags from class and method levels are merged (accumulated). Supports IS_REPEATABLE for multiple #[Tags] on same element. --- composer.json | 4 +- composer.lock | 65 ++++++++++++----------- docs/usage.md | 40 ++++++++++++++ examples/tests/Unit/AttributeTest.php | 3 ++ examples/tests/Unit/CombinedTest.php | 2 + expected/codeception-examples.yaml | 9 ++++ src/Attributes/AttributeParser.php | 4 ++ src/Attributes/Tags.php | 31 +++++++++++ src/Attributes/TagsAttributeInterface.php | 11 ++++ src/Models/Metadata.php | 1 + src/Reporter.php | 1 + tests/ClassTagsFixture.php | 16 ++++++ tests/TagsAttributeTest.php | 60 +++++++++++++++++++++ tests/TagsFixture.php | 30 +++++++++++ 14 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 src/Attributes/Tags.php create mode 100644 src/Attributes/TagsAttributeInterface.php create mode 100644 tests/ClassTagsFixture.php create mode 100644 tests/TagsAttributeTest.php create mode 100644 tests/TagsFixture.php diff --git a/composer.json b/composer.json index 2adcca1..978a9ff 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ ], "require": { "php": "^8.0", - "qase/php-commons": "^2.1.12", + "qase/php-commons": "^2.1.18", "codeception/codeception": "^5.2" }, "require-dev": { @@ -35,7 +35,7 @@ "Tests\\": "tests/" } }, - "version": "2.1.4", + "version": "2.1.5", "config": { "optimize-autoloader": true, "preferred-install": { diff --git a/composer.lock b/composer.lock index a2e279d..6b6a1b3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8367e5e32744aacd4f2973a90ab72e26", + "content-hash": "0651a36e553b630d37aab3aa98b70f0d", "packages": [ { "name": "behat/gherkin", @@ -91,16 +91,16 @@ }, { "name": "brick/math", - "version": "0.14.1", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", - "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -139,7 +139,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.1" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -147,7 +147,7 @@ "type": "github" } ], - "time": "2025-11-24T14:40:29+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "codeception/codeception", @@ -582,16 +582,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -607,6 +607,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { @@ -678,7 +679,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -694,7 +695,7 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-03-10T16:41:02+00:00" }, { "name": "myclabs/deep-copy", @@ -1715,22 +1716,22 @@ }, { "name": "qase/php-commons", - "version": "2.1.12", + "version": "2.1.18", "source": { "type": "git", "url": "https://github.com/qase-tms/qase-php-commons.git", - "reference": "bf177809b99c6d3bb4dc961ac5661b01919e5e54" + "reference": "ca0459b6b97a7810a7aa1db15297f864827556d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/qase-tms/qase-php-commons/zipball/bf177809b99c6d3bb4dc961ac5661b01919e5e54", - "reference": "bf177809b99c6d3bb4dc961ac5661b01919e5e54", + "url": "https://api.github.com/repos/qase-tms/qase-php-commons/zipball/ca0459b6b97a7810a7aa1db15297f864827556d0", + "reference": "ca0459b6b97a7810a7aa1db15297f864827556d0", "shasum": "" }, "require": { "php": "^8.0", "qase/qase-api-client": "^1.1.5", - "qase/qase-api-v2-client": "^1.1.2", + "qase/qase-api-v2-client": "^1.1.5", "ramsey/uuid": "^4.7" }, "require-dev": { @@ -1764,22 +1765,22 @@ ], "support": { "issues": "https://github.com/qase-tms/qase-php-commons/issues", - "source": "https://github.com/qase-tms/qase-php-commons/tree/v2.1.12" + "source": "https://github.com/qase-tms/qase-php-commons/tree/v2.1.18" }, - "time": "2026-01-27T13:30:32+00:00" + "time": "2026-04-09T08:02:34+00:00" }, { "name": "qase/qase-api-client", - "version": "1.1.5", + "version": "1.1.10", "source": { "type": "git", "url": "https://github.com/qase-tms/qase-api-client.git", - "reference": "2a95fe3c8a09a3d733338cde79c3f97ce74ea6d2" + "reference": "edde038fcc858cd342eb900c076524798f635772" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/qase-tms/qase-api-client/zipball/2a95fe3c8a09a3d733338cde79c3f97ce74ea6d2", - "reference": "2a95fe3c8a09a3d733338cde79c3f97ce74ea6d2", + "url": "https://api.github.com/repos/qase-tms/qase-api-client/zipball/edde038fcc858cd342eb900c076524798f635772", + "reference": "edde038fcc858cd342eb900c076524798f635772", "shasum": "" }, "require": { @@ -1820,22 +1821,22 @@ ], "support": { "issues": "https://github.com/qase-tms/qase-api-client/issues", - "source": "https://github.com/qase-tms/qase-api-client/tree/v1.1.5" + "source": "https://github.com/qase-tms/qase-api-client/tree/v1.1.10" }, - "time": "2025-09-23T12:19:27+00:00" + "time": "2026-03-26T13:32:24+00:00" }, { "name": "qase/qase-api-v2-client", - "version": "1.1.2", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/qase-tms/qase-api-v2-client.git", - "reference": "1d70ae5efc88fdb24af60bd24e3f0352237ee86c" + "reference": "9cc363fc035bd66b8bdae5cf894525136d660000" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/qase-tms/qase-api-v2-client/zipball/1d70ae5efc88fdb24af60bd24e3f0352237ee86c", - "reference": "1d70ae5efc88fdb24af60bd24e3f0352237ee86c", + "url": "https://api.github.com/repos/qase-tms/qase-api-v2-client/zipball/9cc363fc035bd66b8bdae5cf894525136d660000", + "reference": "9cc363fc035bd66b8bdae5cf894525136d660000", "shasum": "" }, "require": { @@ -1876,9 +1877,9 @@ ], "support": { "issues": "https://github.com/qase-tms/qase-api-v2-client/issues", - "source": "https://github.com/qase-tms/qase-api-v2-client/tree/v1.1.2" + "source": "https://github.com/qase-tms/qase-api-v2-client/tree/v1.1.5" }, - "time": "2025-08-13T07:36:21+00:00" + "time": "2026-04-07T12:51:13+00:00" }, { "name": "ralouphie/getallheaders", diff --git a/docs/usage.md b/docs/usage.md index 2e7e062..cdf077a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -76,6 +76,44 @@ public function testLogin(): void } ``` +#### Tags +Adds tags to the test case. Can be used on classes and methods. Tags from both levels are merged. + +```php +use Qase\Codeception\Attributes\Tags; + +// Single attribute with multiple tags +#[Tags('smoke', 'regression')] +public function testLogin(): void +{ + $this->assertTrue(true); +} + +// Multiple Tags attributes +#[Tags('smoke')] +#[Tags('regression')] +public function testLogin(): void +{ + $this->assertTrue(true); +} + +// Class-level + method-level merge +#[Tags('smoke')] +class AuthTest extends \Codeception\Test\Unit +{ + #[Tags('regression')] + public function testLogin(): void + { + // Gets both 'smoke' and 'regression' tags + } + + public function testLogout(): void + { + // Gets only 'smoke' tag (inherited from class) + } +} +``` + #### Parameter Adds parameters to the test case. Useful for parameterized tests. @@ -101,6 +139,7 @@ use Qase\Codeception\Attributes\QaseId; use Qase\Codeception\Attributes\Title; use Qase\Codeception\Attributes\Suite; use Qase\Codeception\Attributes\Field; +use Qase\Codeception\Attributes\Tags; use Qase\Codeception\Attributes\Parameter; use Tests\Support\UnitTester; @@ -114,6 +153,7 @@ class LoginTest extends \Codeception\Test\Unit #[Suite('Critical Features')] #[Field('description', 'Test verifies successful user login')] #[Field('severity', 'high')] + #[Tags('smoke')] #[Parameter('user_type', 'admin')] public function testSuccessfulLogin(): void { diff --git a/examples/tests/Unit/AttributeTest.php b/examples/tests/Unit/AttributeTest.php index fffeda3..e7ec8c2 100644 --- a/examples/tests/Unit/AttributeTest.php +++ b/examples/tests/Unit/AttributeTest.php @@ -8,9 +8,11 @@ use Qase\Codeception\Attributes\QaseId; use Qase\Codeception\Attributes\QaseIds; use Qase\Codeception\Attributes\Suite; +use Qase\Codeception\Attributes\Tags; use Qase\Codeception\Attributes\Title; use Tests\Support\UnitTester; +#[Tags("smoke")] class AttributeTest extends \Codeception\Test\Unit { protected UnitTester $tester; @@ -64,6 +66,7 @@ public function testWithSuites(): void #[Field('severity', 'critical')] #[Field('priority', 'high')] #[Field('layer', 'unit')] + #[Tags('regression')] public function testWithFields(): void { $this->assertCount(3, [1, 2, 3]); diff --git a/examples/tests/Unit/CombinedTest.php b/examples/tests/Unit/CombinedTest.php index 15230c9..4048d05 100644 --- a/examples/tests/Unit/CombinedTest.php +++ b/examples/tests/Unit/CombinedTest.php @@ -8,6 +8,7 @@ use Qase\Codeception\Attributes\Parameter; use Qase\Codeception\Attributes\QaseId; use Qase\Codeception\Attributes\Suite; +use Qase\Codeception\Attributes\Tags; use Qase\Codeception\Attributes\Title; use Tests\Support\UnitTester; @@ -21,6 +22,7 @@ class CombinedTest extends \Codeception\Test\Unit #[Suite('Combined')] #[Field('severity', 'critical')] #[Field('priority', 'high')] + #[Tags('smoke')] #[Parameter('user', 'admin')] #[Parameter('role', 'superuser')] public function testFullMetadata(): void diff --git a/expected/codeception-examples.yaml b/expected/codeception-examples.yaml index d9336ad..770958d 100644 --- a/expected/codeception-examples.yaml +++ b/expected/codeception-examples.yaml @@ -15,6 +15,7 @@ results: fields: severity: critical priority: high + tags: smoke params: user: admin role: superuser @@ -51,6 +52,7 @@ results: testops_ids: - 4 status: skipped + tags: smoke relations: suite: data: @@ -67,6 +69,7 @@ results: testops_ids: - 1 status: passed + tags: smoke relations: suite: data: @@ -83,6 +86,7 @@ results: testops_ids: - 8 status: passed + tags: smoke relations: suite: data: @@ -95,6 +99,7 @@ results: testops_ids: - 5 status: passed + tags: smoke relations: suite: data: @@ -111,6 +116,7 @@ results: testops_ids: - 3 status: invalid + tags: smoke relations: suite: data: @@ -150,6 +156,7 @@ results: severity: critical priority: high layer: unit + tags: smoke,regression relations: suite: data: @@ -166,6 +173,7 @@ results: testops_ids: - 2 status: failed + tags: smoke relations: suite: data: @@ -183,6 +191,7 @@ results: - 6 - 7 status: passed + tags: smoke relations: suite: data: diff --git a/src/Attributes/AttributeParser.php b/src/Attributes/AttributeParser.php index 022d973..165c7df 100644 --- a/src/Attributes/AttributeParser.php +++ b/src/Attributes/AttributeParser.php @@ -83,6 +83,10 @@ private function getMetadataFromAnnotations(array $annotations): Metadata if ($annotation instanceof ParameterAttributeInterface) { $metadata->parameters[$annotation->getName()] = $annotation->getValue(); } + + if ($annotation instanceof TagsAttributeInterface) { + $metadata->tags = array_merge($metadata->tags, $annotation->getTags()); + } } return $metadata; diff --git a/src/Attributes/Tags.php b/src/Attributes/Tags.php new file mode 100644 index 0000000..ae4c3ac --- /dev/null +++ b/src/Attributes/Tags.php @@ -0,0 +1,31 @@ +assertTrue(true); + * } + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +final class Tags implements TagsAttributeInterface +{ + /** @var string[] */ + private array $tags; + + public function __construct(string ...$values) + { + $this->tags = $values; + } + + public function getTags(): array + { + return $this->tags; + } +} diff --git a/src/Attributes/TagsAttributeInterface.php b/src/Attributes/TagsAttributeInterface.php new file mode 100644 index 0000000..c73fb9e --- /dev/null +++ b/src/Attributes/TagsAttributeInterface.php @@ -0,0 +1,11 @@ +title = $metadata->title ?? $test->getName(); $result->params = $metadata->parameters; $result->fields = $metadata->fields; + $result->tags = $metadata->tags; $result->signature = $this->createSignature($event, $metadata->qaseIds, $metadata->parameters); $result->execution->thread = "main"; $result->relations = $relation; diff --git a/tests/ClassTagsFixture.php b/tests/ClassTagsFixture.php new file mode 100644 index 0000000..eadcfe9 --- /dev/null +++ b/tests/ClassTagsFixture.php @@ -0,0 +1,16 @@ +parser = new AttributeParser(new Logger(), new AttributeReader()); + } + + public function testParseTagsFromSingleAttribute(): void + { + $metadata = $this->parser->parseAttribute(TagsFixture::class, 'testWithTags'); + $this->assertSame(['smoke', 'regression'], $metadata->tags); + } + + public function testMergeClassAndMethodTags(): void + { + $metadata = $this->parser->parseAttribute(ClassTagsFixture::class, 'testWithMethodTags'); + $this->assertSame(['smoke', 'regression'], $metadata->tags); + } + + public function testEmptyTags(): void + { + $metadata = $this->parser->parseAttribute(TagsFixture::class, 'testWithoutTags'); + $this->assertSame([], $metadata->tags); + } + + public function testParseTagsFromMultipleAttributes(): void + { + $metadata = $this->parser->parseAttribute(TagsFixture::class, 'testWithMultipleTags'); + $this->assertSame(['smoke', 'regression'], $metadata->tags); + } + + public function testClassTagsInheritedByMethodWithoutTags(): void + { + $metadata = $this->parser->parseAttribute(ClassTagsFixture::class, 'testWithoutTags'); + $this->assertSame(['smoke'], $metadata->tags); + } + + public function testAllAttributesTogether(): void + { + $metadata = $this->parser->parseAttribute(TagsFixture::class, 'testWithAll'); + $this->assertSame('Custom title', $metadata->title); + $this->assertSame([100], $metadata->qaseIds); + $this->assertSame(['Auth'], $metadata->suites); + $this->assertSame(['severity' => 'high'], $metadata->fields); + $this->assertSame(['smoke', 'regression'], $metadata->tags); + } +} diff --git a/tests/TagsFixture.php b/tests/TagsFixture.php new file mode 100644 index 0000000..c06389b --- /dev/null +++ b/tests/TagsFixture.php @@ -0,0 +1,30 @@ +