Skip to content

Commit

Permalink
Add ArchUnit test for consistency of repeatable annotations
Browse files Browse the repository at this point in the history
Issues: #4059 and #4063
  • Loading branch information
marcphilipp committed Oct 9, 2024
1 parent 8a96d2b commit eba399e
Showing 1 changed file with 59 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,50 @@
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;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
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))) //
Expand All @@ -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
Expand Down Expand Up @@ -94,6 +120,16 @@ void avoidAccessingStandardStreams(JavaClasses classes) {
GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS.check(subset);
}

private static ArchCondition<? super JavaClass> haveContainerAnnotationWithSameRetentionPolicy() {
return ArchCondition.from(new RepeatableAnnotationPredicate<>(Retention.class,
(expectedTarget, actualTarget) -> expectedTarget.value() == actualTarget.value()));
}

private static ArchCondition<? super JavaClass> haveContainerAnnotationWithSameTargetTypes() {
return ArchCondition.from(new RepeatableAnnotationPredicate<>(Target.class,
(expectedTarget, actualTarget) -> Arrays.equals(expectedTarget.value(), actualTarget.value())));
}

static class AllJars implements LocationProvider {

@Override
Expand All @@ -103,4 +139,27 @@ public Set<Location> get(Class<?> testClass) {

}

private static class RepeatableAnnotationPredicate<T extends Annotation> extends DescribedPredicate<JavaClass> {

private final Class<T> annotationType;
private final BiPredicate<T, T> predicate;

public RepeatableAnnotationPredicate(Class<T> annotationType, BiPredicate<T, T> 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());
}
}
}

0 comments on commit eba399e

Please sign in to comment.