From 806661b49b3696aaecb5e1536eb93d6644ca172c Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 19 Nov 2025 13:54:07 +0300 Subject: [PATCH 1/2] Improve Attribute annotations and their extraction --- CHANGELOG.md | 4 + .../annotations/attribute/Attribute.java | 15 ++- .../annotations/attribute/AttributeGroup.java | 30 +++++ .../annotations/attribute/AttributeValue.java | 15 ++- .../attribute/AttributeValueGroup.java | 30 +++++ .../annotations/attribute/Attributes.java | 2 +- .../attribute/MultiKeyAttribute.java | 17 ++- .../attribute/MultiKeyAttributeGroup.java | 30 +++++ .../attribute/MultiValueAttribute.java | 19 ++- .../attribute/MultiValueAttributeGroup.java | 30 +++++ .../reportportal/utils/AttributeParser.java | 108 +++++++++++++++--- .../utils/AnnotationAttributeParserTest.java | 69 +++++++++++ 12 files changed, 319 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java create mode 100644 src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java create mode 100644 src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java create mode 100644 src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 573a1ee9..a390b48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added +- `AttributeParser.retrieveAttributes(Executable)` method, by @HardNorth +### Changed +- Attribute annotations and their extraction improved, by @HardNorth ## [5.4.7] ### Added diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java b/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java index cdc7a099..6cf4fbbf 100644 --- a/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java +++ b/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java @@ -16,18 +16,17 @@ package com.epam.reportportal.annotations.attribute; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; /** - * Annotation used in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} - * with both 'key' and 'value' fields specified - * - * @author Ivan Budayeu + * Annotation used per se or in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} + * with both 'key' and 'value' fields specified. */ +@Inherited +@Documented @Retention(RetentionPolicy.RUNTIME) -@Target({}) +@Repeatable(AttributeGroup.class) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) public @interface Attribute { String key(); diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java new file mode 100644 index 00000000..2f9ffdc1 --- /dev/null +++ b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.annotations.attribute; + +import java.lang.annotation.*; + +/** + * Gathering annotation for {@link Attribute} + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) +public @interface AttributeGroup { + Attribute[] value(); +} diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java index 21b319a4..29e35a7d 100644 --- a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java +++ b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java @@ -16,18 +16,17 @@ package com.epam.reportportal.annotations.attribute; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; /** - * Annotation used in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} - * with only 'value' field specified ('key' in the resulted entity will be NULL) - * - * @author Ivan Budayeu + * Annotation used per se or in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} + * with only 'value' field specified ('key' in the resulted entity will be NULL). In this case it will appear as a tag on ReportPortal. */ +@Inherited +@Documented @Retention(RetentionPolicy.RUNTIME) -@Target({}) +@Repeatable(AttributeValueGroup.class) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) public @interface AttributeValue { String value(); diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java new file mode 100644 index 00000000..644f1cb1 --- /dev/null +++ b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.annotations.attribute; + +import java.lang.annotation.*; + +/** + * Gathering annotation for {@link AttributeValue} + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) +public @interface AttributeValueGroup { + AttributeValue[] value(); +} diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java b/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java index 5e509e38..35ae67b5 100644 --- a/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java +++ b/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java @@ -28,7 +28,7 @@ * @see MultiValueAttribute */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE }) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) @Inherited public @interface Attributes { diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java index c48c8cad..e192642e 100644 --- a/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java +++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java @@ -16,19 +16,18 @@ package com.epam.reportportal.annotations.attribute; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; /** - * Annotation used in {@link Attributes} as field, to provide multiple {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} - * with both 'key' and 'value' fields specified. - * Used to prevent duplication of {@link Attribute} annotation with the same value and different keys - * - * @author Ivan Budayeu + * Annotation used per se or in {@link Attributes} as field, to provide multiple + * {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} with both 'key' and 'value' fields specified. + * Used to prevent duplication of {@link Attribute} annotation with the same value and different keys. */ +@Inherited +@Documented @Retention(RetentionPolicy.RUNTIME) -@Target({}) +@Repeatable(MultiKeyAttributeGroup.class) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) public @interface MultiKeyAttribute { String[] keys(); diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java new file mode 100644 index 00000000..ca2227d5 --- /dev/null +++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.annotations.attribute; + +import java.lang.annotation.*; + +/** + * Gathering annotation for {@link MultiKeyAttribute} + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) +public @interface MultiKeyAttributeGroup { + MultiKeyAttribute[] value(); +} diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java index 39186326..ecaa72d5 100644 --- a/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java +++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java @@ -16,19 +16,18 @@ package com.epam.reportportal.annotations.attribute; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.*; /** - * Annotation used in {@link Attributes} as field, to provide multiple {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} - * with both 'key' (optional) and 'value' fields specified. - * Used to prevent duplication of {@link Attribute} annotation with the same key and different values - * - * @author Ivan Budayeu + * Annotation used per se or in {@link Attributes} as field, to provide multiple + * {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} with both 'key' (optional) and 'value' fields specified. + * Used to prevent duplication of {@link Attribute} annotation with the same key and different values. */ +@Inherited +@Documented @Retention(RetentionPolicy.RUNTIME) -@Target({}) +@Repeatable(MultiValueAttributeGroup.class) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) public @interface MultiValueAttribute { String key() default ""; @@ -36,7 +35,7 @@ String[] values(); /** - * @return 'true' if key of the resulted entity should be NULL, otherwise {@link MultiValueAttribute#key()} will be used + * @return if key of the resulted entity should be {@code null}, otherwise {@link MultiValueAttribute#key()} will be used. */ boolean isNullKey() default false; } diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java new file mode 100644 index 00000000..f1cf648b --- /dev/null +++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.annotations.attribute; + +import java.lang.annotation.*; + +/** + * Gathering annotation for {@link MultiValueAttribute} + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) +public @interface MultiValueAttributeGroup { + MultiValueAttribute[] value(); +} diff --git a/src/main/java/com/epam/reportportal/utils/AttributeParser.java b/src/main/java/com/epam/reportportal/utils/AttributeParser.java index 98ef372a..ac4c5db2 100644 --- a/src/main/java/com/epam/reportportal/utils/AttributeParser.java +++ b/src/main/java/com/epam/reportportal/utils/AttributeParser.java @@ -21,6 +21,7 @@ import jakarta.annotation.Nullable; import org.apache.commons.lang3.StringUtils; +import java.lang.reflect.Executable; import java.util.*; import java.util.stream.Collectors; @@ -74,7 +75,7 @@ public static Set parseAsSet(@Nullable String rawAttributes) { */ @Nullable public static ItemAttributesRQ splitKeyValue(@Nullable String attribute) { - if (null == attribute || attribute.trim().isEmpty()) { + if (StringUtils.isBlank(attribute)) { return null; } String[] keyValue = attribute.split(KEY_VALUE_SPLITTER); @@ -97,25 +98,87 @@ public static ItemAttributesRQ splitKeyValue(@Nullable String attribute) { * @return a set of ReportPortal attributes */ @Nonnull - public static Set retrieveAttributes(@Nonnull Attributes attributesAnnotation) { + public static Set retrieveAttributes(@Nullable Attributes attributesAnnotation) { Set itemAttributes = new LinkedHashSet<>(); - for (Attribute attribute : attributesAnnotation.attributes()) { - if (!attribute.value().trim().isEmpty()) { - itemAttributes.add(createItemAttribute(attribute.key(), attribute.value())); - } + if (attributesAnnotation == null) { + return itemAttributes; } - for (AttributeValue attributeValue : attributesAnnotation.attributeValues()) { - if (!attributeValue.value().trim().isEmpty()) { - itemAttributes.add(createItemAttribute(null, attributeValue.value())); + itemAttributes.addAll(retrieveAttributes(attributesAnnotation.attributes())); + itemAttributes.addAll(retrieveAttributes(attributesAnnotation.attributeValues())); + itemAttributes.addAll(retrieveAttributes(attributesAnnotation.multiKeyAttributes())); + itemAttributes.addAll(retrieveAttributes(attributesAnnotation.multiValueAttributes())); + return itemAttributes; + } + + /** + * Parse ReportPortal attributes from {@link Attribute} annotations. + * + * @param attributes annotation instances + * @return a set of ReportPortal attributes + */ + @Nonnull + public static Set retrieveAttributes(@Nullable Attribute... attributes) { + Set itemAttributes = new LinkedHashSet<>(); + if (attributes != null) { + for (Attribute attribute : attributes) { + if (StringUtils.isNotBlank(attribute.value())) { + itemAttributes.add(createItemAttribute(attribute.key(), attribute.value())); + } } } - for (MultiKeyAttribute attribute : attributesAnnotation.multiKeyAttributes()) { - itemAttributes.addAll(createItemAttributes(attribute.keys(), attribute.value())); + return itemAttributes; + } + + /** + * Parse ReportPortal attributes from {@link AttributeValue} annotations. + * + * @param attributeValues annotation instances + * @return a set of ReportPortal attributes + */ + @Nonnull + public static Set retrieveAttributes(@Nullable AttributeValue... attributeValues) { + Set itemAttributes = new LinkedHashSet<>(); + if (attributeValues != null) { + for (AttributeValue attributeValue : attributeValues) { + if (StringUtils.isNotBlank(attributeValue.value())) { + itemAttributes.add(createItemAttribute(null, attributeValue.value())); + } + } } - for (MultiValueAttribute attribute : attributesAnnotation.multiValueAttributes()) { - itemAttributes.addAll(createItemAttributes(attribute.isNullKey() ? null : attribute.key(), attribute.values())); + return itemAttributes; + } + + /** + * Parse ReportPortal attributes from {@link MultiKeyAttribute} annotations. + * + * @param multiKeyAttributes annotation instances + * @return a set of ReportPortal attributes + */ + @Nonnull + public static Set retrieveAttributes(@Nullable MultiKeyAttribute... multiKeyAttributes) { + Set itemAttributes = new LinkedHashSet<>(); + if (multiKeyAttributes != null) { + for (MultiKeyAttribute attribute : multiKeyAttributes) { + itemAttributes.addAll(createItemAttributes(attribute.keys(), attribute.value())); + } } + return itemAttributes; + } + /** + * Parse ReportPortal attributes from {@link MultiValueAttribute} annotations. + * + * @param multiValueAttributes annotation instances + * @return a set of ReportPortal attributes + */ + @Nonnull + public static Set retrieveAttributes(@Nullable MultiValueAttribute... multiValueAttributes) { + Set itemAttributes = new LinkedHashSet<>(); + if (multiValueAttributes != null) { + for (MultiValueAttribute attribute : multiValueAttributes) { + itemAttributes.addAll(createItemAttributes(attribute.isNullKey() ? null : attribute.key(), attribute.values())); + } + } return itemAttributes; } @@ -128,7 +191,7 @@ public static Set retrieveAttributes(@Nonnull Attributes attri */ @Nonnull public static List createItemAttributes(@Nullable String[] keys, @Nullable String value) { - if (value == null || value.trim().isEmpty()) { + if (StringUtils.isBlank(value)) { return Collections.emptyList(); } if (keys == null || keys.length < 1) { @@ -164,4 +227,21 @@ public static List createItemAttributes(@Nullable String key, public static ItemAttributesRQ createItemAttribute(@Nullable String key, @Nonnull String value) { return new ItemAttributesRQ(key, value); } + + /** + * Scan for attributes annotations on the given executable and its declaration. + * + * @param executable the executable to scan + * @return a set of ReportPortal attributes or null if not found + */ + @Nonnull + public static Set retrieveAttributes(@Nonnull Executable executable) { + Set itemAttributes = new LinkedHashSet<>(); + itemAttributes.addAll(retrieveAttributes(executable.getAnnotation(Attributes.class))); + itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(Attribute.class))); + itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(AttributeValue.class))); + itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(MultiKeyAttribute.class))); + itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(MultiValueAttribute.class))); + return itemAttributes; + } } diff --git a/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java b/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java index 0ea2db37..0a138a37 100644 --- a/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java +++ b/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java @@ -325,4 +325,73 @@ public void verify_all_attributes_in_one_annotation() throws NoSuchMethodExcepti assertThat(result, hasSize(4)); } + + private static final class StandaloneAttributeVerify { + @Attribute(key = "standaloneKey", value = "standaloneValue") + public void testMethod() { + } + } + + @Test + public void verify_standalone_attribute_on_method() throws NoSuchMethodException { + java.lang.reflect.Method method = StandaloneAttributeVerify.class.getMethod("testMethod"); + Set result = AttributeParser.retrieveAttributes(method); + + assertThat(result, hasSize(1)); + ItemAttributesRQ request = result.iterator().next(); + assertThat(request.getKey(), equalTo("standaloneKey")); + assertThat(request.getValue(), equalTo("standaloneValue")); + } + + private static final class RepeatedAttributeVerify { + @Attribute(key = "key1", value = "value1") + @Attribute(key = "key2", value = "value2") + public void testMethod() { + } + } + + @Test + public void verify_repeated_attribute_on_method() throws NoSuchMethodException { + java.lang.reflect.Method method = RepeatedAttributeVerify.class.getMethod("testMethod"); + Set result = AttributeParser.retrieveAttributes(method); + + assertThat(result, hasSize(2)); + assertThat(result.stream().map(KEY_EXTRACT).collect(toList()), containsInAnyOrder("key1", "key2")); + assertThat(result.stream().map(VALUE_EXTRACT).collect(toList()), containsInAnyOrder("value1", "value2")); + } + + private static final class MixedAttributeVerify { + @Attributes(attributes = @Attribute(key = "innerKey", value = "innerValue")) + @Attribute(key = "outerKey", value = "outerValue") + public void testMethod() { + } + } + + @Test + public void verify_mixed_attributes_on_method() throws NoSuchMethodException { + java.lang.reflect.Method method = MixedAttributeVerify.class.getMethod("testMethod"); + Set result = AttributeParser.retrieveAttributes(method); + + assertThat(result, hasSize(2)); + assertThat(result.stream().map(KEY_EXTRACT).collect(toList()), containsInAnyOrder("innerKey", "outerKey")); + assertThat(result.stream().map(VALUE_EXTRACT).collect(toList()), containsInAnyOrder("innerValue", "outerValue")); + } + + private static final class AllTypesOnMethodVerify { + @Attribute(key = "attrKey", value = "attrValue") + @AttributeValue("attrValueOnly") + @MultiKeyAttribute(keys = { "mk1", "mk2" }, value = "mkValue") + @MultiValueAttribute(key = "mvKey", values = { "mv1", "mv2" }) + public void testMethod() { + } + } + + @Test + public void verify_all_types_on_method() throws NoSuchMethodException { + java.lang.reflect.Method method = AllTypesOnMethodVerify.class.getMethod("testMethod"); + Set result = AttributeParser.retrieveAttributes(method); + + // 1 (Attribute) + 1 (AttributeValue) + 2 (MultiKey) + 2 (MultiValue) = 6 + assertThat(result, hasSize(6)); + } } From 4e458ddf2f5be2ada1fad99f6f7f23950f6a4072 Mon Sep 17 00:00:00 2001 From: Vadzim Hushchanskou Date: Wed, 19 Nov 2025 15:10:48 +0300 Subject: [PATCH 2/2] Update src/main/java/com/epam/reportportal/utils/AttributeParser.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/com/epam/reportportal/utils/AttributeParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/epam/reportportal/utils/AttributeParser.java b/src/main/java/com/epam/reportportal/utils/AttributeParser.java index ac4c5db2..ae7341ae 100644 --- a/src/main/java/com/epam/reportportal/utils/AttributeParser.java +++ b/src/main/java/com/epam/reportportal/utils/AttributeParser.java @@ -232,7 +232,7 @@ public static ItemAttributesRQ createItemAttribute(@Nullable String key, @Nonnul * Scan for attributes annotations on the given executable and its declaration. * * @param executable the executable to scan - * @return a set of ReportPortal attributes or null if not found + * @return a non-null set of ReportPortal attributes (may be empty if none are found) */ @Nonnull public static Set retrieveAttributes(@Nonnull Executable executable) {