Skip to content

Commit c12634c

Browse files
authored
Support explicit ClassLoader in ClassSelector and MethodSelector
This commit introduces overloaded selectClass() and selectMethod() variants in DiscoverySelectors that accept an explicit ClassLoader and also modifies ClassSelector and MethodSelector accordingly. Closes #1987 Closes #2107
1 parent b3f6c99 commit c12634c

File tree

7 files changed

+172
-19
lines changed

7 files changed

+172
-19
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ repository on GitHub.
3232
which return a `Stream`.
3333
* New `tryToLoadClass(...)` variant in `ReflectionSupport` that accepts an explicit
3434
`ClassLoader`, allowing classes to be resolved with custom `ClassLoader` arrangements.
35+
* New overloaded constructors for `ClassSelector` and `MethodSelector` that take an
36+
explicit `ClassLoader` as a parameter, allowing selectors to select classes in custom
37+
`ClassLoader` arrangements like in OSGi.
3538
* `ReflectionSupport.findMethod(Class<?>, String, String)` now uses the `ClassLoader` of
3639
the supplied `Class` to load parameter types instead of using the _default_
3740
`ClassLoader`. This allows parameter types to be resolved with custom `ClassLoader`

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClassSelector.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import org.apiguardian.api.API;
1818
import org.junit.platform.commons.PreconditionViolationException;
19+
import org.junit.platform.commons.function.Try;
1920
import org.junit.platform.commons.util.ReflectionUtils;
2021
import org.junit.platform.commons.util.ToStringBuilder;
2122
import org.junit.platform.engine.DiscoverySelector;
@@ -43,15 +44,22 @@
4344
public class ClassSelector implements DiscoverySelector {
4445

4546
private final String className;
47+
private final ClassLoader classLoader;
4648

4749
private Class<?> javaClass;
4850

4951
ClassSelector(String className) {
52+
this(className, null);
53+
}
54+
55+
ClassSelector(String className, ClassLoader classLoader) {
5056
this.className = className;
57+
this.classLoader = classLoader;
5158
}
5259

5360
ClassSelector(Class<?> javaClass) {
5461
this.className = javaClass.getName();
62+
this.classLoader = javaClass.getClassLoader();
5563
this.javaClass = javaClass;
5664
}
5765

@@ -62,6 +70,13 @@ public String getClassName() {
6270
return this.className;
6371
}
6472

73+
/**
74+
* Get the class loader used to load the selected class.
75+
*/
76+
public ClassLoader getClassLoader() {
77+
return this.classLoader;
78+
}
79+
6580
/**
6681
* Get the selected {@link Class}.
6782
*
@@ -71,9 +86,10 @@ public String getClassName() {
7186
*/
7287
public Class<?> getJavaClass() {
7388
if (this.javaClass == null) {
74-
this.javaClass = ReflectionUtils.tryToLoadClass(this.className).getOrThrow(
75-
cause -> new PreconditionViolationException("Could not load class with name: " + this.className,
76-
cause));
89+
final Try<Class<?>> clazz = this.classLoader == null ? ReflectionUtils.tryToLoadClass(this.className)
90+
: ReflectionUtils.tryToLoadClass(className, this.classLoader);
91+
this.javaClass = clazz.getOrThrow(cause -> new PreconditionViolationException(
92+
"Could not load class with name: " + this.className, cause));
7793
}
7894
return this.javaClass;
7995
}

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,21 @@ public static ClassSelector selectClass(String className) {
393393
return new ClassSelector(className);
394394
}
395395

396+
/**
397+
* Create a {@code ClassSelector} for the supplied class name using the
398+
* supplied class loader.
399+
*
400+
* @param className the fully qualified name of the class to select;
401+
* never {@code null} or blank
402+
* @param classLoader the class loader to use to try and load the
403+
* supplied class. If {@code null}, the default class loader will be used.
404+
* @see ClassSelector
405+
*/
406+
public static ClassSelector selectClass(String className, ClassLoader classLoader) {
407+
Preconditions.notBlank(className, "Class name must not be null or blank");
408+
return new ClassSelector(className, classLoader);
409+
}
410+
396411
/**
397412
* Create a {@code MethodSelector} for the supplied <em>fully qualified
398413
* method name</em>.
@@ -436,22 +451,53 @@ public static ClassSelector selectClass(String className) {
436451
* @see MethodSelector
437452
*/
438453
public static MethodSelector selectMethod(String fullyQualifiedMethodName) throws PreconditionViolationException {
454+
return selectMethod(fullyQualifiedMethodName, (ClassLoader) null);
455+
}
456+
457+
/**
458+
* Create a {@code MethodSelector} for the supplied <em>fully qualified
459+
* method name</em> using the given classloader.
460+
*
461+
* @param fullyQualifiedMethodName the fully qualified name of the method to select; never
462+
* {@code null} or blank (see {@link #selectMethod(String)} for the format of this string).
463+
* @param classLoader the class loader to use to try and load the
464+
* supplied class. If {@code null}, the default class loader will be used.
465+
* @see #selectMethod(String)
466+
*/
467+
public static MethodSelector selectMethod(String fullyQualifiedMethodName, ClassLoader classLoader)
468+
throws PreconditionViolationException {
439469
String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName);
440-
return selectMethod(methodParts[0], methodParts[1], methodParts[2]);
470+
return selectMethod(methodParts[0], methodParts[1], methodParts[2], classLoader);
441471
}
442472

443473
/**
444-
* Create a {@code MethodSelector} for the supplied class name and method name.
474+
* Create a {@code MethodSelector} for the supplied class name and method name
475+
* using the default class loader.
445476
*
446477
* @param className the fully qualified name of the class in which the method
447478
* is declared, or a subclass thereof; never {@code null} or blank
448479
* @param methodName the name of the method to select; never {@code null} or blank
449480
* @see MethodSelector
450481
*/
451482
public static MethodSelector selectMethod(String className, String methodName) {
483+
return selectMethod(className, methodName, (ClassLoader) null);
484+
}
485+
486+
/**
487+
* Create a {@code MethodSelector} for the supplied class name and method name, using
488+
* the given class loader to find the class.
489+
*
490+
* @param className the fully qualified name of the class in which the method
491+
* is declared, or a subclass thereof; never {@code null} or blank
492+
* @param methodName the name of the method to select; never {@code null} or blank
493+
* @param classLoader the class loader to use to try and load the
494+
* supplied class. If {@code null}, the default class loader will be used.
495+
* @see MethodSelector
496+
*/
497+
public static MethodSelector selectMethod(String className, String methodName, ClassLoader classLoader) {
452498
Preconditions.notBlank(className, "Class name must not be null or blank");
453499
Preconditions.notBlank(methodName, "Method name must not be null or blank");
454-
return new MethodSelector(className, methodName);
500+
return new MethodSelector(className, methodName, classLoader);
455501
}
456502

457503
/**
@@ -471,10 +517,31 @@ public static MethodSelector selectMethod(String className, String methodName) {
471517
* @see MethodSelector
472518
*/
473519
public static MethodSelector selectMethod(String className, String methodName, String methodParameterTypes) {
520+
return selectMethod(className, methodName, methodParameterTypes, null);
521+
}
522+
523+
/**
524+
* Create a {@code MethodSelector} for the supplied class name, method name,
525+
* and method parameter types, using the specified class loader.
526+
*
527+
* <p>The parameter types {@code String} is typically a comma-separated list
528+
* of atomic types, fully qualified class names, or array types; however,
529+
* the exact syntax depends on the underlying test engine.
530+
*
531+
* @param className the fully qualified name of the class in which the method
532+
* is declared, or a subclass thereof; never {@code null} or blank
533+
* @param methodName the name of the method to select; never {@code null} or blank
534+
* @param methodParameterTypes the method parameter types as a single string; never
535+
* {@code null} though potentially an empty string if the method does not accept
536+
* arguments
537+
* @see MethodSelector
538+
*/
539+
public static MethodSelector selectMethod(String className, String methodName, String methodParameterTypes,
540+
ClassLoader classLoader) {
474541
Preconditions.notBlank(className, "Class name must not be null or blank");
475542
Preconditions.notBlank(methodName, "Method name must not be null or blank");
476543
Preconditions.notNull(methodParameterTypes, "Parameter types must not be null");
477-
return new MethodSelector(className, methodName, methodParameterTypes.trim());
544+
return new MethodSelector(className, methodName, methodParameterTypes.trim(), classLoader);
478545
}
479546

480547
/**

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.apiguardian.api.API;
1919
import org.junit.platform.commons.PreconditionViolationException;
20+
import org.junit.platform.commons.function.Try;
2021
import org.junit.platform.commons.util.ClassUtils;
2122
import org.junit.platform.commons.util.ReflectionUtils;
2223
import org.junit.platform.commons.util.StringUtils;
@@ -53,6 +54,7 @@
5354
@API(status = STABLE, since = "1.0")
5455
public class MethodSelector implements DiscoverySelector {
5556

57+
private final ClassLoader classLoader;
5658
private final String className;
5759
private final String methodName;
5860
private final String methodParameterTypes;
@@ -61,10 +63,19 @@ public class MethodSelector implements DiscoverySelector {
6163
private Method javaMethod;
6264

6365
MethodSelector(String className, String methodName) {
64-
this(className, methodName, "");
66+
this(className, methodName, "", (ClassLoader) null);
67+
}
68+
69+
MethodSelector(String className, String methodName, ClassLoader classLoader) {
70+
this(className, methodName, "", classLoader);
6571
}
6672

6773
MethodSelector(String className, String methodName, String methodParameterTypes) {
74+
this(className, methodName, methodParameterTypes, (ClassLoader) null);
75+
}
76+
77+
MethodSelector(String className, String methodName, String methodParameterTypes, ClassLoader classLoader) {
78+
this.classLoader = classLoader;
6879
this.className = className;
6980
this.methodName = methodName;
7081
this.methodParameterTypes = methodParameterTypes;
@@ -75,13 +86,15 @@ public class MethodSelector implements DiscoverySelector {
7586
}
7687

7788
MethodSelector(Class<?> javaClass, String methodName, String methodParameterTypes) {
89+
this.classLoader = javaClass.getClassLoader();
7890
this.javaClass = javaClass;
7991
this.className = javaClass.getName();
8092
this.methodName = methodName;
8193
this.methodParameterTypes = methodParameterTypes;
8294
}
8395

8496
MethodSelector(Class<?> javaClass, Method method) {
97+
this.classLoader = javaClass.getClassLoader();
8598
this.javaClass = javaClass;
8699
this.className = javaClass.getName();
87100
this.javaMethod = method;
@@ -96,6 +109,13 @@ public String getClassName() {
96109
return this.className;
97110
}
98111

112+
/**
113+
* Get the class loader used to load the specified class.
114+
*/
115+
public ClassLoader getClassLoader() {
116+
return this.classLoader;
117+
}
118+
99119
/**
100120
* Get the selected method name.
101121
*/
@@ -153,7 +173,9 @@ public Method getJavaMethod() {
153173
private void lazyLoadJavaClass() {
154174
if (this.javaClass == null) {
155175
// @formatter:off
156-
this.javaClass = ReflectionUtils.tryToLoadClass(this.className).getOrThrow(
176+
final Try<Class<?>> clazz = this.classLoader == null ? ReflectionUtils.tryToLoadClass(this.className)
177+
: ReflectionUtils.tryToLoadClass(className, this.classLoader);
178+
this.javaClass = clazz.getOrThrow(
157179
cause -> new PreconditionViolationException("Could not load class with name: " + this.className, cause));
158180
// @formatter:on
159181
}

platform-tests/src/test/java/org/junit/platform/engine/discovery/ClassSelectorTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,11 @@ void preservesOriginalExceptionWhenTryingToLoadClass() {
4343
assertThat(e).hasMessage("Could not load class with name: org.example.TestClass").hasCauseInstanceOf(
4444
ClassNotFoundException.class);
4545
}
46+
47+
@Test
48+
void useClassClassLoader() {
49+
var selector = new ClassSelector(getClass());
50+
51+
assertThat(selector.getClassLoader()).isNotNull().isSameAs(getClass().getClassLoader());
52+
}
4653
}

platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import static java.lang.String.join;
1414
import static org.assertj.core.api.Assertions.assertThat;
1515
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
17+
import static org.junit.jupiter.api.Assertions.assertSame;
1618
import static org.junit.jupiter.api.Assertions.assertThrows;
1719
import static org.junit.jupiter.params.provider.Arguments.arguments;
1820
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
@@ -31,6 +33,7 @@
3133
import java.io.File;
3234
import java.lang.reflect.Method;
3335
import java.net.URI;
36+
import java.net.URLClassLoader;
3437
import java.nio.file.Path;
3538
import java.nio.file.Paths;
3639
import java.util.List;
@@ -46,6 +49,7 @@
4649
import org.junit.jupiter.params.provider.Arguments;
4750
import org.junit.jupiter.params.provider.MethodSource;
4851
import org.junit.platform.commons.PreconditionViolationException;
52+
import org.junit.platform.commons.test.TestClassLoader;
4953
import org.junit.platform.commons.util.ReflectionUtils;
5054

5155
/**
@@ -242,9 +246,20 @@ void selectClassByName() {
242246
assertEquals(getClass(), selector.getJavaClass());
243247
}
244248

249+
@Test
250+
void selectClassByNameAndClassLoader() throws Exception {
251+
try (TestClassLoader l = TestClassLoader.forClasses(getClass())) {
252+
ClassSelector selector = selectClass(getClass().getName(), l);
253+
assertEquals(getClass().getTypeName(), selector.getJavaClass().getTypeName());
254+
assertNotEquals(getClass(), selector.getJavaClass());
255+
assertSame(l, selector.getClassLoader());
256+
assertSame(l, selector.getJavaClass().getClassLoader());
257+
}
258+
}
259+
245260
@Test
246261
void selectMethodByClassNameAndMethodNamePreconditions() {
247-
assertViolatesPrecondition(() -> selectMethod("TestClass", null));
262+
assertViolatesPrecondition(() -> selectMethod("TestClass", (String) null));
248263
assertViolatesPrecondition(() -> selectMethod("TestClass", ""));
249264
assertViolatesPrecondition(() -> selectMethod("TestClass", " "));
250265
assertViolatesPrecondition(() -> selectMethod((String) null, "method"));
@@ -260,7 +275,7 @@ void selectMethodByClassNameMethodNameAndMethodParameterTypesPreconditions() {
260275
assertViolatesPrecondition(() -> selectMethod((String) null, "method", "int"));
261276
assertViolatesPrecondition(() -> selectMethod("", "method", "int"));
262277
assertViolatesPrecondition(() -> selectMethod(" ", "method", "int"));
263-
assertViolatesPrecondition(() -> selectMethod("TestClass", "method", null));
278+
assertViolatesPrecondition(() -> selectMethod("TestClass", "method", (String) null));
264279
}
265280

266281
@Test
@@ -320,6 +335,22 @@ void selectMethodByFullyQualifiedName() throws Exception {
320335
assertSelectMethodByFullyQualifiedName(clazz, method);
321336
}
322337

338+
@Test
339+
void selectMethodByFullyQualifiedNameAndClassLoader() throws Exception {
340+
try (URLClassLoader l = TestClassLoader.forClasses(getClass())) {
341+
Class<?> clazz = l.loadClass(getClass().getTypeName());
342+
assertNotEquals(getClass(), clazz);
343+
344+
Method method = clazz.getDeclaredMethod("myTest");
345+
MethodSelector selector = selectMethod(getClass().getName(), "myTest", l);
346+
assertEquals(method, selector.getJavaMethod());
347+
assertEquals(clazz, selector.getJavaClass());
348+
assertEquals(clazz.getName(), selector.getClassName());
349+
assertEquals(method.getName(), selector.getMethodName());
350+
assertEquals("", selector.getMethodParameterTypes());
351+
}
352+
}
353+
323354
@Test
324355
void selectMethodByFullyQualifiedNameForDefaultMethodInInterface() throws Exception {
325356
Class<?> clazz = TestCaseWithDefaultMethod.class;

platform-tests/src/test/java/org/junit/platform/engine/discovery/MethodSelectorTests.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,32 @@ class MethodSelectorTests extends AbstractEqualsAndHashCodeTests {
2727

2828
@Test
2929
void equalsAndHashCode() {
30-
var selector1 = new MethodSelector("TestClass", "method", "int, boolean");
31-
var selector2 = new MethodSelector("TestClass", "method", "int, boolean");
30+
var selector1 = new MethodSelector("TestClass", "method", "int, boolean", null);
31+
var selector2 = new MethodSelector("TestClass", "method", "int, boolean", null);
3232

3333
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "method", "int"));
34-
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "method"));
35-
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "X", "int, boolean"));
36-
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "X"));
37-
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("X", "method", "int, boolean"));
38-
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("X", "method"));
34+
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "method", (ClassLoader) null));
35+
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "X", "int, boolean", null));
36+
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("TestClass", "X", (ClassLoader) null));
37+
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("X", "method", "int, boolean", null));
38+
assertEqualsAndHashCode(selector1, selector2, new MethodSelector("X", "method", (ClassLoader) null));
3939
}
4040

4141
@Test
4242
void preservesOriginalExceptionWhenTryingToLoadClass() {
43-
var selector = new MethodSelector("TestClass", "method", "int, boolean");
43+
var selector = new MethodSelector("TestClass", "method", "int, boolean", (ClassLoader) null);
4444

4545
var e = assertThrows(PreconditionViolationException.class, selector::getJavaClass);
4646

4747
assertThat(e).hasMessage("Could not load class with name: TestClass").hasCauseInstanceOf(
4848
ClassNotFoundException.class);
4949
}
5050

51+
@Test
52+
void usesClassClassLoader() {
53+
var selector = new MethodSelector(getClass(), "usesClassClassLoader");
54+
55+
assertThat(selector.getClassLoader()).isNotNull().isEqualTo(getClass().getClassLoader());
56+
}
57+
5158
}

0 commit comments

Comments
 (0)