From 8bc798f00b0bbf9f58a96789bc7bc2cd80404e94 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sat, 13 May 2023 13:22:26 +0200 Subject: [PATCH] Support explicit ClassLoader in NestedMethodSelector Resolves #3298. --- .../release-notes-5.10.0-M1.adoc | 7 +-- .../engine/discovery/DiscoverySelectors.java | 45 +++++++++++++++- .../discovery/NestedMethodSelector.java | 24 ++++++--- .../discovery/DiscoverySelectorsTests.java | 52 ++++++++++++++++++- .../discovery/NestedMethodSelectorTests.java | 43 ++++++++++----- 5 files changed, 145 insertions(+), 26 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc index c837866c0409..fe346720e342 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc @@ -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 diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java index edc38f56eb14..6ada10fb641c 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java @@ -669,11 +669,29 @@ public static NestedClassSelector selectNestedClass(List enclosingClassN @API(status = STABLE, since = "1.6") public static NestedMethodSelector selectNestedMethod(List 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 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); } /** @@ -696,12 +714,35 @@ public static NestedMethodSelector selectNestedMethod(List enclosingClas @API(status = STABLE, since = "1.6") public static NestedMethodSelector selectNestedMethod(List 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 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); } /** diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java index 28f00afa88c1..3e3e97371c41 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java @@ -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; @@ -55,15 +56,15 @@ public class NestedMethodSelector implements DiscoverySelector { private final NestedClassSelector nestedClassSelector; private final MethodSelector methodSelector; - NestedMethodSelector(List enclosingClassNames, String nestedClassName, String methodName) { - this.nestedClassSelector = new NestedClassSelector(enclosingClassNames, nestedClassName); - this.methodSelector = new MethodSelector(nestedClassName, methodName); + NestedMethodSelector(List enclosingClassNames, String nestedClassName, String methodName, + ClassLoader classLoader) { + this(enclosingClassNames, nestedClassName, methodName, "", classLoader); } NestedMethodSelector(List 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> enclosingClasses, Class nestedClass, String methodName) { @@ -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. @@ -182,6 +193,7 @@ public String toString() { .append("nestedClassName", getNestedClassName()) // .append("methodName", getMethodName()) // .append("methodParameterTypes", getMethodParameterTypes()) // + .append("classLoader", getClassLoader()) // .toString(); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java index a6c96f751d91..bf2cd601b4bb 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java @@ -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), @@ -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"; @@ -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 { diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/NestedMethodSelectorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/NestedMethodSelectorTests.java index 2c547172a95f..13a6bf5bab4a 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/NestedMethodSelectorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/NestedMethodSelectorTests.java @@ -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); @@ -64,7 +65,8 @@ 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); @@ -72,4 +74,17 @@ void preservesOriginalExceptionWhenTryingToLoadNestedClass() { .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() { + } + } + }