AnnotationUtil.java
package net.morimekta.testing.junit5;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.stream.Stream;
/**
* Internal utility for resolving annotations from JUnit 5 extension contexts, searching
* method-level first and then walking up the class hierarchy.
*/
public final class AnnotationUtil {
/**
* Check if the given annotation is present on the test method or any enclosing class.
*
* @param context The JUnit extension context.
* @param annotation The annotation type to look for.
* @param <A> The annotation type.
* @return True if the annotation is found.
*/
public static <A extends Annotation> boolean isAnnotationPresent(ExtensionContext context, Class<A> annotation) {
return getTopAnnotation(context, annotation).isPresent();
}
/**
* Find the most specific (method-first, then class hierarchy) instance of an annotation.
*
* @param context The JUnit extension context.
* @param annotation The annotation type to look for.
* @param <A> The annotation type.
* @return The annotation if found, empty otherwise.
*/
public static <A extends Annotation> Optional<A> getTopAnnotation(
ExtensionContext context, Class<A> annotation) {
return context
.getTestMethod()
.map(m -> m.getDeclaredAnnotation(annotation))
.or(() -> context.getTestClass().flatMap(t -> getTopAnnotation(t, annotation)));
}
/**
* Collect all instances of an annotation from the class hierarchy (superclass first)
* and then the test method, in bottom-up order.
*
* @param context The JUnit extension context.
* @param annotation The annotation type to collect.
* @param <T> The annotation type.
* @return A stream of annotations in superclass-to-method order.
*/
public static <T extends Annotation> Stream<T> getAnnotationsBottomUp(
ExtensionContext context, Class<T> annotation) {
return Stream.concat(
context.getTestClass()
.stream()
.flatMap(t -> getAnnotationsBottomUp(t, annotation)),
context.getTestMethod()
.map(m -> m.getDeclaredAnnotation(annotation))
.stream());
}
// --- Private ---
private static <T extends Annotation> Optional<T> getTopAnnotation(
Class<?> type, Class<T> annotation) {
return Optional.ofNullable(type.getDeclaredAnnotation(annotation))
.or(() -> Optional.ofNullable(type.getSuperclass())
.flatMap(t -> getTopAnnotation(t, annotation)));
}
private static <T extends Annotation> Stream<T> getAnnotationsBottomUp(
Class<?> type, Class<T> annotation) {
return Stream.concat(
Optional.ofNullable(type.getSuperclass())
.stream()
.flatMap(t -> getAnnotationsBottomUp(t, annotation)),
Optional.ofNullable(type.getDeclaredAnnotation(annotation))
.stream());
}
private AnnotationUtil() {}
}