Skip to content

Commit d1beb9f

Browse files
committed
Support extension registration via constructor & method params
Issue: #864
1 parent 3f605f1 commit d1beb9f

File tree

8 files changed

+709
-69
lines changed

8 files changed

+709
-69
lines changed

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java

+16-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import static java.util.stream.Collectors.joining;
1414
import static org.apiguardian.api.API.Status.INTERNAL;
1515
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;
16+
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromConstructorParameters;
17+
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters;
1618
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields;
1719
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterAllMethods;
1820
import static org.junit.jupiter.engine.descriptor.LifecycleMethodUtils.findAfterEachMethods;
@@ -152,16 +154,23 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
152154
// one factory registered per class).
153155
this.testInstanceFactory = resolveTestInstanceFactory(registry);
154156

157+
if (this.testInstanceFactory == null) {
158+
registerExtensionsFromConstructorParameters(registry, this.testClass);
159+
}
160+
161+
this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
162+
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
163+
164+
this.beforeAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));
165+
this.afterAllMethods.forEach(method -> registerExtensionsFromExecutableParameters(registry, method));
166+
155167
registerBeforeEachMethodAdapters(registry);
156168
registerAfterEachMethodAdapters(registry);
157169

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

162-
this.beforeAllMethods = findBeforeAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
163-
this.afterAllMethods = findAfterAllMethods(this.testClass, this.lifecycle == Lifecycle.PER_METHOD);
164-
165174
// @formatter:off
166175
return context.extend()
167176
.withTestInstancesProvider(testInstancesProvider(context, extensionContext))
@@ -468,7 +477,10 @@ private void registerAfterEachMethodAdapters(ExtensionRegistrar registrar) {
468477
private void registerMethodsAsExtensions(List<Method> methods, ExtensionRegistrar registrar,
469478
Function<Method, Extension> extensionSynthesizer) {
470479

471-
methods.forEach(method -> registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method));
480+
methods.forEach(method -> {
481+
registerExtensionsFromExecutableParameters(registrar, method);
482+
registrar.registerSyntheticExtension(extensionSynthesizer.apply(method), method);
483+
});
472484
}
473485

474486
private BeforeEachMethodAdapter synthesizeBeforeEachMethodAdapter(Method method) {

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ExtensionUtils.java

+43-1
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields;
1515
import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
1616
import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations;
17+
import static org.junit.platform.commons.util.ReflectionUtils.getDeclaredConstructor;
1718
import static org.junit.platform.commons.util.ReflectionUtils.isNotPrivate;
1819
import static org.junit.platform.commons.util.ReflectionUtils.tryToReadFieldValue;
1920

2021
import java.lang.reflect.AnnotatedElement;
22+
import java.lang.reflect.Executable;
2123
import java.lang.reflect.Field;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
26+
import java.util.Collection;
2427
import java.util.Comparator;
2528
import java.util.List;
29+
import java.util.concurrent.atomic.AtomicInteger;
2630
import java.util.function.Predicate;
2731

2832
import org.junit.jupiter.api.Order;
@@ -78,7 +82,7 @@ static MutableExtensionRegistry populateNewExtensionRegistryFromExtendWithAnnota
7882
}
7983

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

122+
/**
123+
* Register extensions using the supplied registrar from parameters in the
124+
* declared constructor of the supplied class that are annotated with
125+
* {@link ExtendWith @ExtendWith}.
126+
*
127+
* @param registrar the registrar with which to register the extensions; never {@code null}
128+
* @param clazz the class in which to find the declared constructor; never {@code null}
129+
* @since 5.8
130+
*/
131+
static void registerExtensionsFromConstructorParameters(ExtensionRegistrar registrar, Class<?> clazz) {
132+
registerExtensionsFromExecutableParameters(registrar, getDeclaredConstructor(clazz));
133+
}
134+
135+
/**
136+
* Register extensions using the supplied registrar from parameters in the
137+
* supplied {@link Executable} (i.e., a {@link java.lang.reflect.Constructor}
138+
* or {@link java.lang.reflect.Method}) that are annotated with{@link ExtendWith @ExtendWith}.
139+
*
140+
* @param registrar the registrar with which to register the extensions; never {@code null}
141+
* @param executable the constructor or method whose parameters should be searched; never {@code null}
142+
* @since 5.8
143+
*/
144+
static void registerExtensionsFromExecutableParameters(ExtensionRegistrar registrar, Executable executable) {
145+
Preconditions.notNull(registrar, "ExtensionRegistrar must not be null");
146+
Preconditions.notNull(executable, "Executable must not be null");
147+
148+
AtomicInteger index = new AtomicInteger();
149+
150+
// @formatter:off
151+
Arrays.stream(executable.getParameters())
152+
.map(parameter -> findRepeatableAnnotations(parameter, index.getAndIncrement(), ExtendWith.class))
153+
.flatMap(Collection::stream)
154+
.map(ExtendWith::value)
155+
.flatMap(Arrays::stream)
156+
.forEach(registrar::registerExtension);
157+
// @formatter:on
158+
}
159+
118160
/**
119161
* @since 5.4
120162
*/

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static org.apiguardian.api.API.Status.INTERNAL;
1414
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation;
15+
import static org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromExecutableParameters;
1516
import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector;
1617

1718
import java.lang.reflect.Method;
@@ -113,7 +114,10 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
113114
}
114115

115116
protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) {
116-
return populateNewExtensionRegistryFromExtendWithAnnotation(context.getExtensionRegistry(), getTestMethod());
117+
MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation(
118+
context.getExtensionRegistry(), getTestMethod());
119+
registerExtensionsFromExecutableParameters(registry, getTestMethod());
120+
return registry;
117121
}
118122

119123
@Override

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/DefaultParameterContext.java

+3-53
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,7 @@
1010

1111
package org.junit.jupiter.engine.execution;
1212

13-
import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass;
14-
1513
import java.lang.annotation.Annotation;
16-
import java.lang.reflect.AnnotatedElement;
17-
import java.lang.reflect.Constructor;
18-
import java.lang.reflect.Executable;
1914
import java.lang.reflect.Parameter;
2015
import java.util.List;
2116
import java.util.Optional;
@@ -58,62 +53,17 @@ public Optional<Object> getTarget() {
5853

5954
@Override
6055
public boolean isAnnotated(Class<? extends Annotation> annotationType) {
61-
return AnnotationUtils.isAnnotated(getEffectiveAnnotatedParameter(), annotationType);
56+
return AnnotationUtils.isAnnotated(this.parameter, this.index, annotationType);
6257
}
6358

6459
@Override
6560
public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType) {
66-
return AnnotationUtils.findAnnotation(getEffectiveAnnotatedParameter(), annotationType);
61+
return AnnotationUtils.findAnnotation(this.parameter, this.index, annotationType);
6762
}
6863

6964
@Override
7065
public <A extends Annotation> List<A> findRepeatableAnnotations(Class<A> annotationType) {
71-
return AnnotationUtils.findRepeatableAnnotations(getEffectiveAnnotatedParameter(), annotationType);
72-
}
73-
74-
/**
75-
* Due to a bug in {@code javac} on JDK versions prior to JDK 9, looking up
76-
* annotations directly on a {@link Parameter} will fail for inner class
77-
* constructors.
78-
*
79-
* <h4>Bug in {@code javac} on JDK versions prior to JDK 9</h4>
80-
*
81-
* <p>The parameter annotations array in the compiled byte code for the user's
82-
* test class excludes an entry for the implicit <em>enclosing instance</em>
83-
* parameter for an inner class constructor.
84-
*
85-
* <h4>Workaround</h4>
86-
*
87-
* <p>JUnit provides a workaround for this off-by-one error by helping extension
88-
* authors to access annotations on the preceding {@link Parameter} object (i.e.,
89-
* {@code index - 1}). The {@linkplain #getIndex() current index} must never be
90-
* zero in such situations since JUnit Jupiter should never ask a
91-
* {@code ParameterResolver} to resolve a parameter for the implicit <em>enclosing
92-
* instance</em> parameter.
93-
*
94-
* <h4>WARNING</h4>
95-
*
96-
* <p>The {@code AnnotatedElement} returned by this method should never be cast and
97-
* treated as a {@code Parameter} since the metadata (e.g., {@link Parameter#getName()},
98-
* {@link Parameter#getType()}, etc.) will not match those for the declared parameter
99-
* at the given index in an inner class constructor.
100-
*
101-
* @return the actual {@code Parameter} for this context, or the <em>effective</em>
102-
* {@code Parameter} if the aforementioned bug is detected
103-
*/
104-
private AnnotatedElement getEffectiveAnnotatedParameter() {
105-
Executable executable = getDeclaringExecutable();
106-
107-
if (executable instanceof Constructor && isInnerClass(executable.getDeclaringClass())
108-
&& executable.getParameterAnnotations().length == executable.getParameterCount() - 1) {
109-
110-
Preconditions.condition(this.index != 0,
111-
"A ParameterContext should never be created for parameter index 0 in an inner class constructor");
112-
113-
return executable.getParameters()[this.index - 1];
114-
}
115-
116-
return this.parameter;
66+
return AnnotationUtils.findRepeatableAnnotations(this.parameter, this.index, annotationType);
11767
}
11868

11969
@Override

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionRegistrar.java

+12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323
@API(status = INTERNAL, since = "5.5")
2424
public interface ExtensionRegistrar {
2525

26+
/**
27+
* Instantiate an extension of the given type using its default constructor
28+
* and register it in the registry.
29+
*
30+
* <p>A new {@link Extension} should not be registered if an extension of the
31+
* given type already exists in the registry or a parent registry.
32+
*
33+
* @param extensionType the type of extension to register
34+
* @since 5.8
35+
*/
36+
void registerExtension(Class<? extends Extension> extensionType);
37+
2638
/**
2739
* Register the supplied {@link Extension}, without checking if an extension
2840
* of that type has already been registered.

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java

+2-10
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,8 @@ private <E extends Extension> Stream<E> streamLocal(Class<E> extensionType) {
139139
// @formatter:on
140140
}
141141

142-
/**
143-
* Instantiate an extension of the given type using its default constructor
144-
* and register it in this registry.
145-
*
146-
* <p>A new {@link Extension} will not be registered if an extension of the
147-
* given type already exists in this registry or a parent registry.
148-
*
149-
* @param extensionType the type of extension to register
150-
*/
151-
void registerExtension(Class<? extends Extension> extensionType) {
142+
@Override
143+
public void registerExtension(Class<? extends Extension> extensionType) {
152144
if (!isAlreadyRegistered(extensionType)) {
153145
registerLocalExtension(ReflectionUtils.newInstance(extensionType));
154146
}

0 commit comments

Comments
 (0)