From ed1733949ff5d574e8bf759809a7f4db13fc9e52 Mon Sep 17 00:00:00 2001 From: Dmitry Baev Date: Tue, 21 Jul 2026 17:14:21 +0100 Subject: [PATCH] Support composed flaky, muted, and severity annotations across Java test adapters --- .../qameta/allure/util/AnnotationUtils.java | 93 +++++++++++++++- .../allure/util/AnnotationUtilsTest.java | 100 ++++++++++++++++++ .../junitplatform/AllureJunitPlatform.java | 22 ++-- .../AllureJunitPlatformTest.java | 43 ++++++++ .../features/CriticalUnstable.java | 38 +++++++ .../features/FlakyMutedTest.java | 37 +++++++ .../features/MetaAnnotationTest.java | 26 +++++ .../io/qameta/allure/junit4/AllureJunit4.java | 71 +++++++++++-- .../allure/junit4/AllureJunit4Test.java | 83 +++++++++++++++ .../junit4/samples/CriticalUnstable.java | 34 ++++++ .../junit4/samples/FlakyMutedOnClassTest.java | 29 +++++ .../allure/junit4/samples/FlakyMutedTest.java | 38 +++++++ .../junit4/samples/MetaAnnotationTest.java | 26 +++++ .../allure/junit4/samples/SeverityTest.java | 33 ++++++ .../io/qameta/allure/spock2/AllureSpock2.java | 6 ++ .../allure/spock2/AllureSpock2Test.java | 39 +++++++ .../spock2/samples/CriticalUnstable.java | 34 ++++++ .../spock2/samples/MetaAnnotatedTest.groovy | 27 +++++ .../spock2/samples/TestWithSeverity.groovy | 35 ++++++ .../io/qameta/allure/testng/AllureTestNg.java | 23 ++-- .../allure/testng/AllureTestNgTest.java | 18 ++++ .../testng/samples/CriticalUnstable.java | 34 ++++++ .../testng/samples/MetaAnnotationTest.java | 26 +++++ .../test/resources/suites/meta-annotation.xml | 10 ++ 24 files changed, 885 insertions(+), 40 deletions(-) create mode 100644 allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/CriticalUnstable.java create mode 100644 allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/FlakyMutedTest.java create mode 100644 allure-junit-platform/src/test/java/io/qameta/allure/junitplatform/features/MetaAnnotationTest.java create mode 100644 allure-junit4/src/test/java/io/qameta/allure/junit4/samples/CriticalUnstable.java create mode 100644 allure-junit4/src/test/java/io/qameta/allure/junit4/samples/FlakyMutedOnClassTest.java create mode 100644 allure-junit4/src/test/java/io/qameta/allure/junit4/samples/FlakyMutedTest.java create mode 100644 allure-junit4/src/test/java/io/qameta/allure/junit4/samples/MetaAnnotationTest.java create mode 100644 allure-junit4/src/test/java/io/qameta/allure/junit4/samples/SeverityTest.java create mode 100644 allure-spock2/src/test/groovy/io/qameta/allure/spock2/samples/CriticalUnstable.java create mode 100644 allure-spock2/src/test/groovy/io/qameta/allure/spock2/samples/MetaAnnotatedTest.groovy create mode 100644 allure-spock2/src/test/groovy/io/qameta/allure/spock2/samples/TestWithSeverity.groovy create mode 100644 allure-testng/src/test/java/io/qameta/allure/testng/samples/CriticalUnstable.java create mode 100644 allure-testng/src/test/java/io/qameta/allure/testng/samples/MetaAnnotationTest.java create mode 100644 allure-testng/src/test/resources/suites/meta-annotation.xml diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/AnnotationUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/AnnotationUtils.java index 91fec5888..e680d7638 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/util/AnnotationUtils.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/AnnotationUtils.java @@ -19,6 +19,8 @@ import io.qameta.allure.LabelAnnotation; import io.qameta.allure.LinkAnnotation; import io.qameta.allure.Muted; +import io.qameta.allure.Severity; +import io.qameta.allure.SeverityLevel; import io.qameta.allure.model.Label; import io.qameta.allure.model.Link; import org.slf4j.Logger; @@ -32,6 +34,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.stream.Collectors; @@ -46,6 +49,7 @@ * test cases via reflection. * */ +@SuppressWarnings("PMD.GodClass") public final class AnnotationUtils { private static final Logger LOGGER = LoggerFactory.getLogger(AnnotationUtils.class); @@ -57,23 +61,77 @@ private AnnotationUtils() { } /** - * Returns true if {@link io.qameta.allure.Flaky} annotation is present. + * Returns true if {@link io.qameta.allure.Flaky} annotation is present. The annotation is + * also discovered when used as a meta annotation on other (custom) annotations. * * @param annotatedElement the element to search annotations on. * @return true if {@link io.qameta.allure.Flaky} annotation is present, false otherwise. */ public static boolean isFlaky(final AnnotatedElement annotatedElement) { - return annotatedElement.isAnnotationPresent(Flaky.class); + return isFlaky(asList(annotatedElement.getAnnotations())); } /** - * Returns true if {@link io.qameta.allure.Muted} annotation is present. + * Returns true if {@link io.qameta.allure.Flaky} annotation is present within given annotations, + * either directly or as a meta annotation. + * + * @param annotations annotations to analyse. + * @return true if {@link io.qameta.allure.Flaky} annotation is present, false otherwise. + */ + public static boolean isFlaky(final Collection annotations) { + return findMetaAnnotations(Flaky.class, annotations).findAny().isPresent(); + } + + /** + * Returns true if {@link io.qameta.allure.Muted} annotation is present. The annotation is + * also discovered when used as a meta annotation on other (custom) annotations. * * @param annotatedElement the element to search annotations on. * @return true if {@link io.qameta.allure.Muted} annotation is present, false otherwise. */ public static boolean isMuted(final AnnotatedElement annotatedElement) { - return annotatedElement.isAnnotationPresent(Muted.class); + return isMuted(asList(annotatedElement.getAnnotations())); + } + + /** + * Returns true if {@link io.qameta.allure.Muted} annotation is present within given annotations, + * either directly or as a meta annotation. + * + * @param annotations annotations to analyse. + * @return true if {@link io.qameta.allure.Muted} annotation is present, false otherwise. + */ + public static boolean isMuted(final Collection annotations) { + return findMetaAnnotations(Muted.class, annotations).findAny().isPresent(); + } + + /** + * Returns the severity specified by the {@link io.qameta.allure.Severity} annotation. The + * annotation is also discovered when used as a meta annotation on other (custom) annotations. + * + * @param annotatedElement the element to search annotations on. + * @return the discovered severity, if any. + */ + public static Optional getSeverity(final AnnotatedElement annotatedElement) { + return getSeverity(asList(annotatedElement.getAnnotations())); + } + + /** + * Returns the severity specified by the {@link io.qameta.allure.Severity} annotation within + * given annotations. A directly declared {@link io.qameta.allure.Severity} takes precedence; + * otherwise the annotation is also discovered when used as a meta annotation on other (custom) + * annotations. + * + * @param annotations annotations to analyse. + * @return the discovered severity, if any. + */ + public static Optional getSeverity(final Collection annotations) { + return annotations.stream() + .filter(Severity.class::isInstance) + .map(Severity.class::cast) + .findFirst() + .map(Optional::of) + .orElseGet(() -> findMetaAnnotations(Severity.class, annotations).findFirst()) + .map(Severity::value); } /** @@ -169,6 +227,33 @@ private static Stream extractMetaAnnotations( return Stream.empty(); } + // Unlike AnnotatedElement#getAnnotationsByType, discovers annotations of the given type even + // when they are used as a meta annotation on another (custom) annotation. + private static Stream findMetaAnnotations( + final Class annotationType, + final Collection candidates) { + final Set visited = new HashSet<>(); + return candidates.stream() + .flatMap(AnnotationUtils::extractRepeatable) + .flatMap(candidate -> findMetaAnnotations(annotationType, candidate, visited)); + } + + private static Stream findMetaAnnotations( + final Class annotationType, + final Annotation candidate, + final Set visited) { + if (isInJavaLangAnnotationPackage(candidate.annotationType()) || !visited.add(candidate)) { + return Stream.empty(); + } + final Stream current = annotationType.equals(candidate.annotationType()) + ? Stream.of(annotationType.cast(candidate)) + : Stream.empty(); + final Stream children = Stream.of(candidate.annotationType().getAnnotations()) + .flatMap(AnnotationUtils::extractRepeatable) + .flatMap(annotation -> findMetaAnnotations(annotationType, annotation, visited)); + return Stream.concat(current, children); + } + private static Stream