Skip to content

Commit

Permalink
Support explicit ClassLoader in NestedClassSelector
Browse files Browse the repository at this point in the history
Issue: #3298
  • Loading branch information
marcphilipp committed May 13, 2023
1 parent e576cff commit a30437b
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ 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` 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`, and
`MethodSelector` 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 @@ -635,9 +635,26 @@ public static NestedClassSelector selectNestedClass(List<Class<?>> enclosingClas
*/
@API(status = STABLE, since = "1.6")
public static NestedClassSelector selectNestedClass(List<String> enclosingClassNames, String nestedClassName) {
return selectNestedClass(enclosingClassNames, nestedClassName, null);
}

/**
* Create a {@code NestedClassSelector} for the supplied class name, its enclosing
* classes' names, 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 classLoader the class loader to use to load the enclosing and nested classes, or
* {@code null} to signal that the default {@code ClassLoader} should be used
* @since 1.10
* @see NestedClassSelector
*/
@API(status = EXPERIMENTAL, since = "1.10")
public static NestedClassSelector selectNestedClass(List<String> enclosingClassNames, String nestedClassName,
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");
return new NestedClassSelector(enclosingClassNames, nestedClassName);
return new NestedClassSelector(enclosingClassNames, nestedClassName, classLoader);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
package org.junit.platform.engine.discovery;

import static java.util.stream.Collectors.toList;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;

import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -46,17 +48,31 @@
@API(status = STABLE, since = "1.6")
public class NestedClassSelector implements DiscoverySelector {

private List<ClassSelector> enclosingClassSelectors;
private ClassSelector nestedClassSelector;
private final List<ClassSelector> enclosingClassSelectors;
private final ClassSelector nestedClassSelector;
private final ClassLoader classLoader;

NestedClassSelector(List<String> enclosingClassNames, String nestedClassName) {
this.enclosingClassSelectors = enclosingClassNames.stream().map(ClassSelector::new).collect(toList());
this.nestedClassSelector = new ClassSelector(nestedClassName);
NestedClassSelector(List<String> enclosingClassNames, String nestedClassName, ClassLoader classLoader) {
this.enclosingClassSelectors = enclosingClassNames.stream() //
.map(className -> new ClassSelector(className, classLoader)) //
.collect(toUnmodifiableList());
this.nestedClassSelector = new ClassSelector(nestedClassName, classLoader);
this.classLoader = classLoader;
}

NestedClassSelector(List<Class<?>> enclosingClasses, Class<?> nestedClass) {
this.enclosingClassSelectors = enclosingClasses.stream().map(ClassSelector::new).collect(toList());
this.nestedClassSelector = new ClassSelector(nestedClass);
this.classLoader = nestedClass.getClassLoader();
}

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

/**
Expand Down Expand Up @@ -121,6 +137,7 @@ public String toString() {
return new ToStringBuilder(this) //
.append("enclosingClassNames", getEnclosingClassNames()) //
.append("nestedClassName", getNestedClassName()) //
.append("classLoader", getClassLoader()) //
.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,27 @@ void selectNestedClassByClassNames() {
assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName);
}

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

var selector = selectNestedClass(List.of(enclosingClassName), nestedClassName, 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());
}
}

@Test
void selectDoubleNestedClassByClassNames() {
var selector = selectNestedClass(List.of(enclosingClassName, nestedClassName), doubleNestedClassName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ class NestedClassSelectorTests extends AbstractEqualsAndHashCodeTests {
@Test
void equalsAndHashCode() {
var selector1 = new NestedClassSelector(List.of("org.example.EnclosingTestClass"),
"org.example.NestedTestClass");
"org.example.NestedTestClass", null);
var selector2 = new NestedClassSelector(List.of("org.example.EnclosingTestClass"),
"org.example.NestedTestClass");
var selector3 = new NestedClassSelector(List.of("org.example.X"), "org.example.Y");
"org.example.NestedTestClass", null);
var selector3 = new NestedClassSelector(List.of("org.example.X"), "org.example.Y", null);

assertEqualsAndHashCode(selector1, selector2, selector3);
}

@Test
void preservesOriginalExceptionWhenTryingToLoadEnclosingClasses() {
var selector = new NestedClassSelector(List.of("org.example.EnclosingTestClass"),
"org.example.NestedTestClass");
var selector = new NestedClassSelector(List.of("org.example.EnclosingTestClass"), "org.example.NestedTestClass",
null);

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

Expand All @@ -51,12 +51,23 @@ void preservesOriginalExceptionWhenTryingToLoadEnclosingClasses() {

@Test
void preservesOriginalExceptionWhenTryingToLoadNestedClass() {
var selector = new NestedClassSelector(List.of("org.example.EnclosingTestClass"),
"org.example.NestedTestClass");
var selector = new NestedClassSelector(List.of("org.example.EnclosingTestClass"), "org.example.NestedTestClass",
null);

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

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

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

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

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

0 comments on commit a30437b

Please sign in to comment.