Skip to content

Commit

Permalink
Support explicit ClassLoader in NestedMethodSelector
Browse files Browse the repository at this point in the history
Resolves #3298.
  • Loading branch information
marcphilipp committed May 13, 2023
1 parent a30437b commit 8bc798f
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ repository on GitHub.
`ClassLoader`. This allows parameter types to be resolved with custom `ClassLoader`
arrangements (such as OSGi). Consequently, `DiscoverySelectors.selectMethod(Class<?>,
String, String)` also now works properly with custom `ClassLoader` arrangements.
* New overloaded constructors for `ClassSelector`, `NestedClassSelector`, and
`MethodSelector` that take an explicit `ClassLoader` as a parameter, allowing selectors
to select classes in custom `ClassLoader` arrangements like in OSGi.
* New overloaded constructors for `ClassSelector`, `NestedClassSelector`,
`MethodSelector`, and `NestedMethodSelector` that take an explicit `ClassLoader` as a
parameter, allowing selectors to select classes in custom `ClassLoader` arrangements
like in OSGi.
* For consistency with JUnit Jupiter lifecycle callbacks, listener method pairs for
started/finished and opened/closed events are now invoked using "wrapping" semantics.
This means that finished/closed event methods are invoked in reverse order compared to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,11 +669,29 @@ public static NestedClassSelector selectNestedClass(List<String> enclosingClassN
@API(status = STABLE, since = "1.6")
public static NestedMethodSelector selectNestedMethod(List<String> enclosingClassNames, String nestedClassName,
String methodName) {
return selectNestedMethod(enclosingClassNames, nestedClassName, methodName, (ClassLoader) null);
}

/**
* Create a {@code NestedMethodSelector} for the supplied nested class name, method name,
* and class loader.
*
* @param enclosingClassNames the names of the enclosing classes; never {@code null} or empty
* @param nestedClassName the name of the nested class to select; never {@code null} or blank
* @param methodName the name of the method to select; never {@code null} or blank
* @param classLoader the class loader to use to load the method's declaring
* class, or {@code null} to signal that the default {@code ClassLoader}
* should be used
* @since 1.10
* @see NestedMethodSelector
*/
@API(status = EXPERIMENTAL, since = "1.10")
public static NestedMethodSelector selectNestedMethod(List<String> enclosingClassNames, String nestedClassName,
String methodName, ClassLoader classLoader) throws PreconditionViolationException {
Preconditions.notEmpty(enclosingClassNames, "Enclosing class names must not be null or empty");
Preconditions.notBlank(nestedClassName, "Nested class name must not be null or blank");
Preconditions.notBlank(methodName, "Method name must not be null or blank");
return new NestedMethodSelector(enclosingClassNames, nestedClassName, methodName);
return new NestedMethodSelector(enclosingClassNames, nestedClassName, methodName, classLoader);
}

/**
Expand All @@ -696,12 +714,35 @@ public static NestedMethodSelector selectNestedMethod(List<String> enclosingClas
@API(status = STABLE, since = "1.6")
public static NestedMethodSelector selectNestedMethod(List<String> enclosingClassNames, String nestedClassName,
String methodName, String methodParameterTypes) {
return selectNestedMethod(enclosingClassNames, nestedClassName, methodName, methodParameterTypes, null);
}

/**
* Create a {@code NestedMethodSelector} for the supplied nested class name, method name,
* method parameter types, and class loader.
*
* @param enclosingClassNames the names of the enclosing classes; never {@code null} or empty
* @param nestedClassName the name of the nested class to select; never {@code null} or blank
* @param methodName the name of the method to select; never {@code null} or blank
* @param methodParameterTypes the method parameter types as a single string; never
* {@code null} though potentially an empty string if the method does not accept
* arguments
* @param classLoader the class loader to use to load the method's declaring
* class, or {@code null} to signal that the default {@code ClassLoader}
* should be used
* @since 1.10
* @see #selectNestedMethod(List, String, String, String)
*/
@API(status = EXPERIMENTAL, since = "1.10")
public static NestedMethodSelector selectNestedMethod(List<String> enclosingClassNames, String nestedClassName,
String methodName, String methodParameterTypes, ClassLoader classLoader) {

Preconditions.notEmpty(enclosingClassNames, "Enclosing class names must not be null or empty");
Preconditions.notBlank(nestedClassName, "Nested class name must not be null or blank");
Preconditions.notBlank(methodName, "Method name must not be null or blank");
Preconditions.notNull(methodParameterTypes, "Parameter types must not be null");
return new NestedMethodSelector(enclosingClassNames, nestedClassName, methodName, methodParameterTypes);
return new NestedMethodSelector(enclosingClassNames, nestedClassName, methodName, methodParameterTypes,
classLoader);
}

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

package org.junit.platform.engine.discovery;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

import java.lang.reflect.Method;
Expand Down Expand Up @@ -55,15 +56,15 @@ public class NestedMethodSelector implements DiscoverySelector {
private final NestedClassSelector nestedClassSelector;
private final MethodSelector methodSelector;

NestedMethodSelector(List<String> enclosingClassNames, String nestedClassName, String methodName) {
this.nestedClassSelector = new NestedClassSelector(enclosingClassNames, nestedClassName);
this.methodSelector = new MethodSelector(nestedClassName, methodName);
NestedMethodSelector(List<String> enclosingClassNames, String nestedClassName, String methodName,
ClassLoader classLoader) {
this(enclosingClassNames, nestedClassName, methodName, "", classLoader);
}

NestedMethodSelector(List<String> enclosingClassNames, String nestedClassName, String methodName,
String methodParameterTypes) {
this.nestedClassSelector = new NestedClassSelector(enclosingClassNames, nestedClassName);
this.methodSelector = new MethodSelector(nestedClassName, methodName, methodParameterTypes);
String methodParameterTypes, ClassLoader classLoader) {
this.nestedClassSelector = new NestedClassSelector(enclosingClassNames, nestedClassName, classLoader);
this.methodSelector = new MethodSelector(nestedClassName, methodName, methodParameterTypes, classLoader);
}

NestedMethodSelector(List<Class<?>> enclosingClasses, Class<?> nestedClass, String methodName) {
Expand All @@ -82,6 +83,16 @@ public class NestedMethodSelector implements DiscoverySelector {
this.methodSelector = new MethodSelector(nestedClass, method);
}

/**
* Get the {@link ClassLoader} used to load the nested class.
*
* @since 1.10
*/
@API(status = EXPERIMENTAL, since = "1.10")
public ClassLoader getClassLoader() {
return this.nestedClassSelector.getClassLoader();
}

/**
* Get the names of the classes enclosing the nested class
* containing the selected method.
Expand Down Expand Up @@ -182,6 +193,7 @@ public String toString() {
.append("nestedClassName", getNestedClassName()) //
.append("methodName", getMethodName()) //
.append("methodParameterTypes", getMethodParameterTypes()) //
.append("classLoader", getClassLoader()) //
.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,30 @@ void selectNestedMethodByEnclosingClassNamesAndMethodName() throws Exception {
assertThat(selector.getMethodName()).isEqualTo(methodName);
}

@Test
void selectNestedMethodByEnclosingClassNamesAndMethodNameAndClassLoader() throws Exception {
var testClasses = List.of(AbstractClassWithNestedInnerClass.class, ClassWithNestedInnerClass.class,
AbstractClassWithNestedInnerClass.NestedClass.class);
try (var testClassLoader = TestClassLoader.forClasses(testClasses.toArray(Class[]::new))) {

var selector = selectNestedMethod(List.of(enclosingClassName), nestedClassName, methodName,
testClassLoader);

assertThat(selector.getEnclosingClasses()).doesNotContain(ClassWithNestedInnerClass.class);
assertThat(selector.getEnclosingClasses()).extracting(Class::getName).containsOnly(enclosingClassName);
assertThat(selector.getNestedClass()).isNotEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class);
assertThat(selector.getNestedClass().getName()).isEqualTo(nestedClassName);

assertThat(selector.getClassLoader()).isSameAs(testClassLoader);
assertThat(selector.getEnclosingClasses()).extracting(Class::getClassLoader).containsOnly(
testClassLoader);
assertThat(selector.getNestedClass().getClassLoader()).isSameAs(testClassLoader);
assertSame(testClassLoader, selector.getNestedClass().getClassLoader());

assertThat(selector.getMethodName()).isEqualTo(methodName);
}
}

@Test
void selectNestedMethodByEnclosingClassesAndMethodName() throws Exception {
var selector = selectNestedMethod(List.of(ClassWithNestedInnerClass.class),
Expand Down Expand Up @@ -776,6 +800,31 @@ void selectNestedMethodByEnclosingClassNamesAndMethodNameWithParameterTypes() th
assertThat(selector.getMethodName()).isEqualTo(methodName);
}

@Test
void selectNestedMethodByEnclosingClassNamesAndMethodNameWithParameterTypesAndClassLoader() throws Exception {
var testClasses = List.of(AbstractClassWithNestedInnerClass.class, ClassWithNestedInnerClass.class,
AbstractClassWithNestedInnerClass.NestedClass.class);
try (var testClassLoader = TestClassLoader.forClasses(testClasses.toArray(Class[]::new))) {

var selector = selectNestedMethod(List.of(enclosingClassName), nestedClassName, methodName,
String.class.getName(), testClassLoader);

assertThat(selector.getEnclosingClasses()).doesNotContain(ClassWithNestedInnerClass.class);
assertThat(selector.getEnclosingClasses()).extracting(Class::getName).containsOnly(enclosingClassName);
assertThat(selector.getNestedClass()).isNotEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class);
assertThat(selector.getNestedClass().getName()).isEqualTo(nestedClassName);

assertThat(selector.getClassLoader()).isSameAs(testClassLoader);
assertThat(selector.getEnclosingClasses()).extracting(Class::getClassLoader).containsOnly(
testClassLoader);
assertThat(selector.getNestedClass().getClassLoader()).isSameAs(testClassLoader);
assertSame(testClassLoader, selector.getNestedClass().getClassLoader());

assertThat(selector.getMethodName()).isEqualTo(methodName);
assertThat(selector.getMethodParameterTypes()).isEqualTo(String.class.getName());
}
}

@Test
void selectDoubleNestedMethodByEnclosingClassNamesAndMethodName() throws Exception {
var doubleNestedMethodName = "doubleNestedTest";
Expand Down Expand Up @@ -808,7 +857,8 @@ void selectNestedMethodPreconditions() {
assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", null, "int"));
assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", " "));
assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", " ", "int"));
assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", "methodName", null));
assertViolatesPrecondition(
() -> selectNestedMethod(List.of("ClassName"), "ClassName", "methodName", (String) null));
}

abstract class AbstractClassWithNestedInnerClass {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,33 @@ class NestedMethodSelectorTests extends AbstractEqualsAndHashCodeTests {

@Test
void equalsAndHashCode() {
var selector1 = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method",
"int, boolean");
var selector2 = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method",
"int, boolean");
var selector1 = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int, boolean",
null);
var selector2 = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int, boolean",
null);

assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("X"), "NestedTestClass", "method", "int, boolean"));
new NestedMethodSelector(List.of("X"), "NestedTestClass", "method", "int, boolean", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("X"), "NestedTestClass", "method"));
new NestedMethodSelector(List.of("X"), "NestedTestClass", "method", "", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int"));
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method"));
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "X", "int, boolean"));
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "X", "int, boolean", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "X"));
new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "X", "", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("EnclosingClass"), "X", "method", "int, boolean"));
new NestedMethodSelector(List.of("EnclosingClass"), "X", "method", "int, boolean", null));
assertEqualsAndHashCode(selector1, selector2,
new NestedMethodSelector(List.of("EnclosingClass"), "X", "method"));
new NestedMethodSelector(List.of("EnclosingClass"), "X", "method", "", null));
}

@Test
void preservesOriginalExceptionWhenTryingToLoadEnclosingClass() {
var selector = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int, boolean");
var selector = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int, boolean",
null);

var exception = assertThrows(PreconditionViolationException.class, selector::getEnclosingClasses);

Expand All @@ -64,12 +65,26 @@ void preservesOriginalExceptionWhenTryingToLoadEnclosingClass() {

@Test
void preservesOriginalExceptionWhenTryingToLoadNestedClass() {
var selector = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int, boolean");
var selector = new NestedMethodSelector(List.of("EnclosingClass"), "NestedTestClass", "method", "int, boolean",
null);

var exception = assertThrows(PreconditionViolationException.class, selector::getNestedClass);

assertThat(exception).hasMessage("Could not load class with name: NestedTestClass") //
.hasCauseInstanceOf(ClassNotFoundException.class);
}

@Test
void usesClassClassLoader() {
var selector = new NestedMethodSelector(List.of(getClass()), NestedTestCase.class, "method", "");

assertThat(selector.getClassLoader()).isNotNull().isSameAs(getClass().getClassLoader());
}

@SuppressWarnings("InnerClassMayBeStatic")
class NestedTestCase {
void method() {
}
}

}

0 comments on commit 8bc798f

Please sign in to comment.