Skip to content

Commit

Permalink
Support extension registration via constructor & method params
Browse files Browse the repository at this point in the history
Issue: #864
  • Loading branch information
sbrannen committed Aug 17, 2021
1 parent 3f605f1 commit d1beb9f
Show file tree
Hide file tree
Showing 8 changed files with 709 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import static java.util.stream.Collectors.joining;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields;
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterAllMethods;
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods;
Expand Down Expand Up @@ -152,16 +154,23 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
// one factory registered per class).
this.testInstanceFactory = resolveTestInstanceFactory(registry);

if (this.testInstanceFactory == null) {
registerExtensionsFromConstructorParameters(registry, this.testClass);
}

this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);

this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));
this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));

registerBeforeEachMethodAdapters(registry);
registerAfterEachMethodAdapters(registry);

ThrowableCollector throwableCollector = createThrowableCollector();
ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(),
context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), throwableCollector);

this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);

// @formatter:off
return context.extend()
.withTestInstancesProvider(testInstancesProvider(context, extensionContext))
Expand Down Expand Up @@ -468,7 +477,10 @@ private void registerAfterEachMethodAdapters(ExtensionRegistrar registrar) {
private void registerMethodsAsExtensions(List<Method> methods, ExtensionRegistrar registrar,
Function<Method, Extension> extensionSynthesizer) {

methods.forEach(method -> registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method));
methods.forEach(method -> {
registerExtensionsFromExecutableParameters(registrar, method);
registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method);
});
}

private BeforeEachMethodAdapter synthesizeBeforeEachMethodAdapter(Method method) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields;
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations;
import static org.junit.platform.commons.util.ReflectionUtils.getDeclaredConstructor;
import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate;
import static org.junit.platform.commons.util.ReflectionUtils.tryToReadFieldValue;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Executable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;

import org.junit.jupiter.api.Order;
Expand Down Expand Up @@ -78,7 +82,7 @@ static MutableExtensionRegistry populateNewExtensionRegistryFromExtendWithAnnota
}

/**
* Register extensions in the supplied registry from fields in the supplied
* Register extensions using the supplied registrar from fields in the supplied
* class that are annotated with {@link RegisterExtension @RegisterExtension}.
*
* <p>The extensions will be sorted according to {@link Order @Order} semantics
Expand Down Expand Up @@ -115,6 +119,44 @@ static void registerExtensionsFromFields(ExtensionRegistrar registrar, Class<?>
});
}

/**
* Register extensions using the supplied registrar from parameters in the
* declared constructor of the supplied class that are annotated with
* {@link ExtendWith @ExtendWith}.
*
* @param registrar the registrar with which to register the extensions; never {@code null}
* @param clazz the class in which to find the declared constructor; never {@code null}
* @since 5.8
*/
static void registerExtensionsFromConstructorParameters(ExtensionRegistrar registrar, Class<?> clazz) {
registerExtensionsFromExecutableParameters(registrar, getDeclaredConstructor(clazz));
}

/**
* Register extensions using the supplied registrar from parameters in the
* supplied {@link Executable} (i.e., a {@link java.lang.reflect.Constructor}
* or {@link java.lang.reflect.Method}) that are annotated with{@link ExtendWith @ExtendWith}.
*
* @param registrar the registrar with which to register the extensions; never {@code null}
* @param executable the constructor or method whose parameters should be searched; never {@code null}
* @since 5.8
*/
static void registerExtensionsFromExecutableParameters(ExtensionRegistrar registrar, Executable executable) {
Preconditions.notNull(registrar, "ExtensionRegistrar must not be null");
Preconditions.notNull(executable, "Executable must not be null");

AtomicInteger index = new AtomicInteger();

// @formatter:off
Arrays.stream(executable.getParameters())
.map(parameter -> findRepeatableAnnotations(parameter, index.getAndIncrement(), ExtendWith.class))
.flatMap(Collection::stream)
.map(ExtendWith::value)
.flatMap(Arrays::stream)
.forEach(registrar::registerExtension);
// @formatter:on
}

/**
* @since 5.4
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static org.apiguardian.api.API.Status.INTERNAL;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters;
import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector;

import java.lang.reflect.Method;
Expand Down Expand Up @@ -113,7 +114,10 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
}

protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) {
return populateNewExtensionRegistryFromExtendWithAnnotation(context.getExtensionRegistry(), getTestMethod());
MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation(
context.getExtensionRegistry(), getTestMethod());
registerExtensionsFromExecutableParameters(registry, getTestMethod());
return registry;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@

package org.junit.jupiter.engine.execution;

import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -58,62 +53,17 @@ public Optional<Object> getTarget() {

@Override
public boolean isAnnotated(Class<? extends Annotation> annotationType) {
return AnnotationUtils.isAnnotated(getEffectiveAnnotatedParameter(), annotationType);
return AnnotationUtils.isAnnotated(this.parameter, this.index, annotationType);
}

@Override
public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType) {
return AnnotationUtils.findAnnotation(getEffectiveAnnotatedParameter(), annotationType);
return AnnotationUtils.findAnnotation(this.parameter, this.index, annotationType);
}

@Override
public <A extends Annotation> List<A> findRepeatableAnnotations(Class<A> annotationType) {
return AnnotationUtils.findRepeatableAnnotations(getEffectiveAnnotatedParameter(), annotationType);
}

/**
* Due to a bug in {@code javac} on JDK versions prior to JDK 9, looking up
* annotations directly on a {@link Parameter} will fail for inner class
* constructors.
*
* <h4>Bug in {@code javac} on JDK versions prior to JDK 9</h4>
*
* <p>The parameter annotations array in the compiled byte code for the user's
* test class excludes an entry for the implicit <em>enclosing instance</em>
* parameter for an inner class constructor.
*
* <h4>Workaround</h4>
*
* <p>JUnit provides a workaround for this off-by-one error by helping extension
* authors to access annotations on the preceding {@link Parameter} object (i.e.,
* {@code index - 1}). The {@linkplain #getIndex() current index} must never be
* zero in such situations since JUnit Jupiter should never ask a
* {@code ParameterResolver} to resolve a parameter for the implicit <em>enclosing
* instance</em> parameter.
*
* <h4>WARNING</h4>
*
* <p>The {@code AnnotatedElement} returned by this method should never be cast and
* treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()},
* {@link Parameter#getType()}, etc.) will not match those for the declared parameter
* at the given index in an inner class constructor.
*
* @return the actual {@code Parameter} for this context, or the <em>effective</em>
* {@code Parameter} if the aforementioned bug is detected
*/
private AnnotatedElement getEffectiveAnnotatedParameter() {
Executable executable = getDeclaringExecutable();

if (executable instanceof Constructor && isInnerClass(executable.getDeclaringClass())
&& executable.getParameterAnnotations().length == executable.getParameterCount() - 1) {

Preconditions.condition(this.index != 0,
"A ParameterContext should never be created for parameter index 0 in an inner class constructor");

return executable.getParameters()[this.index - 1];
}

return this.parameter;
return AnnotationUtils.findRepeatableAnnotations(this.parameter, this.index, annotationType);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
@API(status = INTERNAL, since = "5.5")
public interface ExtensionRegistrar {

/**
* Instantiate an extension of the given type using its default constructor
* and register it in the registry.
*
* <p>A new {@link Extension} should not be registered if an extension of the
* given type already exists in the registry or a parent registry.
*
* @param extensionType the type of extension to register
* @since 5.8
*/
void registerExtension(Class<? extends Extension> extensionType);

/**
* Register the supplied {@link Extension}, without checking if an extension
* of that type has already been registered.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,8 @@ private <E extends Extension> Stream<E> streamLocal(Class<E> extensionType) {
// @formatter:on
}

/**
* Instantiate an extension of the given type using its default constructor
* and register it in this registry.
*
* <p>A new {@link Extension} will not be registered if an extension of the
* given type already exists in this registry or a parent registry.
*
* @param extensionType the type of extension to register
*/
void registerExtension(Class<? extends Extension> extensionType) {
@Override
public void registerExtension(Class<? extends Extension> extensionType) {
if (!isAlreadyRegistered(extensionType)) {
registerLocalExtension(ReflectionUtils.newInstance(extensionType));
}
Expand Down
Loading

0 comments on commit d1beb9f

Please sign in to comment.