From 09cd8b35982a1890285d09cae18828f4c967fcf2 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 9 Oct 2024 14:49:51 +0200 Subject: [PATCH] Add ArchUnit test for consistency of repeatable annotations Issues: #4059 and #4063 (cherry picked from commit eba399e73a75f0edcef6ee6f4b0ce03313134f36) --- .../tooling/support/tests/ArchUnitTests.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java index ff32c0777736..deef517d9f3b 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java @@ -17,10 +17,13 @@ import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleName; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type; import static com.tngtech.archunit.core.domain.JavaModifier.PUBLIC; +import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith; import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.modifier; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameContaining; +import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameStartingWith; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; @@ -28,24 +31,36 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static platform.tooling.support.Helper.loadJarFiles; +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Arrays; import java.util.Set; +import java.util.function.BiPredicate; import java.util.stream.Collectors; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.LocationProvider; +import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.library.GeneralCodingRules; import org.apiguardian.api.API; import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.provider.ArgumentsSource; @Order(Integer.MAX_VALUE) @AnalyzeClasses(locations = ArchUnitTests.AllJars.class) class ArchUnitTests { + @SuppressWarnings("unused") @ArchTest private final ArchRule allPublicTopLevelTypesHaveApiAnnotations = classes() // .that(have(modifier(PUBLIC))) // @@ -55,6 +70,17 @@ class ArchUnitTests { .and(not(describe("are shadowed", resideInAnyPackage("..shadow..")))) // .should().beAnnotatedWith(API.class); + @SuppressWarnings("unused") + @ArchTest // Consistency of @Documented and @Inherited is checked by the compiler but not for @Retention and @Target + private final ArchRule repeatableAnnotationsShouldHaveMatchingContainerAnnotations = classes() // + .that(nameStartingWith("org.junit.")) // + .and().areAnnotations() // + .and().areAnnotatedWith(Repeatable.class) // + .and(are(not(type(ExtendWith.class)))) // to be resolved in https://github.com/junit-team/junit5/issues/4059 + .and(are(not(type(ArgumentsSource.class).or(annotatedWith(ArgumentsSource.class))))) // to be resolved in https://github.com/junit-team/junit5/issues/4063 + .should(haveContainerAnnotationWithSameRetentionPolicy()) // + .andShould(haveContainerAnnotationWithSameTargetTypes()); + @ArchTest void allAreIn(JavaClasses classes) { // about 928 classes found in all jars @@ -94,6 +120,16 @@ void avoidAccessingStandardStreams(JavaClasses classes) { GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS.check(subset); } + private static ArchCondition haveContainerAnnotationWithSameRetentionPolicy() { + return ArchCondition.from(new RepeatableAnnotationPredicate<>(Retention.class, + (expectedTarget, actualTarget) -> expectedTarget.value() == actualTarget.value())); + } + + private static ArchCondition haveContainerAnnotationWithSameTargetTypes() { + return ArchCondition.from(new RepeatableAnnotationPredicate<>(Target.class, + (expectedTarget, actualTarget) -> Arrays.equals(expectedTarget.value(), actualTarget.value()))); + } + static class AllJars implements LocationProvider { @Override @@ -103,4 +139,27 @@ public Set get(Class testClass) { } + private static class RepeatableAnnotationPredicate extends DescribedPredicate { + + private final Class annotationType; + private final BiPredicate predicate; + + public RepeatableAnnotationPredicate(Class annotationType, BiPredicate predicate) { + super("have identical @%s annotation as container annotation", annotationType.getSimpleName()); + this.annotationType = annotationType; + this.predicate = predicate; + } + + @Override + public boolean test(JavaClass annotationClass) { + var containerAnnotationClass = (JavaClass) annotationClass.getAnnotationOfType( + Repeatable.class.getName()).get("value").orElseThrow(); + var expectedAnnotation = annotationClass.tryGetAnnotationOfType(annotationType); + var actualAnnotation = containerAnnotationClass.tryGetAnnotationOfType(annotationType); + return expectedAnnotation.map(expectedTarget -> actualAnnotation // + .map(actualTarget -> predicate.test(expectedTarget, actualTarget)) // + .orElse(false)) // + .orElse(actualAnnotation.isEmpty()); + } + } }