Skip to content

Commit e5167c6

Browse files
authored
Merge pull request quarkusio#50164 from mkouba/component-classloading
QuarkusComponentTest: class loading refactoring
2 parents 7fb7e50 + e88c55f commit e5167c6

File tree

19 files changed

+1416
-729
lines changed

19 files changed

+1416
-729
lines changed

core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.io.InputStream;
99
import java.lang.reflect.Constructor;
1010
import java.lang.reflect.InvocationTargetException;
11+
import java.lang.reflect.Method;
1112
import java.lang.reflect.Modifier;
1213
import java.nio.file.Files;
1314
import java.nio.file.Path;
@@ -89,6 +90,9 @@ public class JunitTestRunner {
8990
public static final DotName QUARKUS_TEST = DotName.createSimple("io.quarkus.test.junit.QuarkusTest");
9091
public static final DotName QUARKUS_MAIN_TEST = DotName.createSimple("io.quarkus.test.junit.main.QuarkusMainTest");
9192
public static final DotName QUARKUS_INTEGRATION_TEST = DotName.createSimple("io.quarkus.test.junit.QuarkusIntegrationTest");
93+
public static final DotName QUARKUS_COMPONENT_TEST = DotName.createSimple("io.quarkus.test.component.QuarkusComponentTest");
94+
public static final DotName QUARKUS_COMPONENT_TEST_EXTENSION = DotName
95+
.createSimple("io.quarkus.test.component.QuarkusComponentTestExtension");
9296
public static final DotName TEST_PROFILE = DotName.createSimple("io.quarkus.test.junit.TestProfile");
9397
public static final DotName TEST = DotName.createSimple(Test.class.getName());
9498
public static final DotName REPEATED_TEST = DotName.createSimple(RepeatedTest.class.getName());
@@ -99,6 +103,7 @@ public class JunitTestRunner {
99103
public static final DotName NESTED = DotName.createSimple(Nested.class.getName());
100104
private static final String ARCHUNIT_FIELDSOURCE_FQCN = "com.tngtech.archunit.junit.FieldSource";
101105
private static final String FACADE_CLASS_LOADER_NAME = "io.quarkus.test.junit.classloading.FacadeClassLoader";
106+
private static final String TEST_DISCOVERY_PROPERTY = "quarkus.continuous-tests-discovery";
102107

103108
private final long runId;
104109
private final DevModeContext.ModuleInfo moduleInfo;
@@ -568,6 +573,9 @@ private DiscoveryResult discoverTestClasses() {
568573
//for now this is out of scope, we are just going to do annotation based discovery
569574
//we will need to fix this sooner rather than later though
570575

576+
// Set the system property that is used for QuarkusComponentTest
577+
System.setProperty(TEST_DISCOVERY_PROPERTY, "true");
578+
571579
if (moduleInfo.getTest().isEmpty()) {
572580
return DiscoveryResult.EMPTY;
573581
}
@@ -613,6 +621,22 @@ private DiscoveryResult discoverTestClasses() {
613621
}
614622
}
615623

624+
Set<String> quarkusComponentTestClasses = new HashSet<>();
625+
for (AnnotationInstance a : index.getAnnotations(QUARKUS_COMPONENT_TEST)) {
626+
DotName name = a.target().asClass().name();
627+
quarkusComponentTestClasses.add(name.toString());
628+
for (ClassInfo subclass : index.getAllKnownSubclasses(name)) {
629+
quarkusComponentTestClasses.add(subclass.name().toString());
630+
}
631+
}
632+
for (ClassInfo clazz : index.getKnownUsers(QUARKUS_COMPONENT_TEST_EXTENSION)) {
633+
DotName name = clazz.name();
634+
quarkusComponentTestClasses.add(name.toString());
635+
for (ClassInfo subclass : index.getAllKnownSubclasses(name)) {
636+
quarkusComponentTestClasses.add(subclass.name().toString());
637+
}
638+
}
639+
616640
// The FacadeClassLoader approach of loading test classes with the classloader we will use to run them can only work for `@QuarkusTest` and not main or integration tests
617641
// Most logic in the JUnitRunner counts main tests as quarkus tests, so do a (mildly irritating) special pass to get the ones which are strictly @QuarkusTest
618642

@@ -676,7 +700,8 @@ private DiscoveryResult discoverTestClasses() {
676700
for (DotName testClass : allTestClasses) {
677701
String name = testClass.toString();
678702
if (integrationTestClasses.contains(name)
679-
|| quarkusTestClasses.contains(name)) {
703+
|| quarkusTestClasses.contains(name)
704+
|| quarkusComponentTestClasses.contains(name)) {
680705
continue;
681706
}
682707
var enclosing = enclosingClasses.get(testClass);
@@ -705,11 +730,14 @@ private DiscoveryResult discoverTestClasses() {
705730
// if we didn't find any test classes, let's return early
706731
// Make sure you also update the logic for the non-empty case above if you adjust this part
707732
if (testType == TestType.ALL) {
708-
if (unitTestClasses.isEmpty() && quarkusTestClasses.isEmpty()) {
733+
if (unitTestClasses.isEmpty()
734+
&& quarkusTestClasses.isEmpty()
735+
&& quarkusComponentTestClasses.isEmpty()) {
709736
return DiscoveryResult.EMPTY;
710737
}
711738
} else if (testType == TestType.UNIT) {
712-
if (unitTestClasses.isEmpty()) {
739+
if (unitTestClasses.isEmpty()
740+
&& quarkusComponentTestClasses.isEmpty()) {
713741
return DiscoveryResult.EMPTY;
714742
}
715743
} else if (quarkusTestClasses.isEmpty()) {
@@ -784,8 +812,9 @@ public String apply(Class<?> aClass) {
784812
return testProfile.value().asClass().name().toString() + "$$" + aClass.getName();
785813
}
786814
}));
815+
787816
QuarkusClassLoader cl = null;
788-
if (!unitTestClasses.isEmpty()) {
817+
if (!unitTestClasses.isEmpty() || !quarkusComponentTestClasses.isEmpty()) {
789818
//we need to work the unit test magic
790819
//this is a lot more complex
791820
//we need to transform the classes to make the tracing magic work
@@ -810,6 +839,7 @@ public String apply(Class<?> aClass) {
810839
cl = testApplication.createDeploymentClassLoader();
811840
deploymentClassLoader = cl;
812841
cl.reset(Collections.emptyMap(), transformedClasses);
842+
813843
for (String i : unitTestClasses) {
814844
try {
815845
utClasses.add(cl.loadClass(i));
@@ -820,6 +850,30 @@ public String apply(Class<?> aClass) {
820850
}
821851
}
822852

853+
if (!quarkusComponentTestClasses.isEmpty()) {
854+
try {
855+
// We use the deployment class loader to load the test class
856+
Class<?> qcfcClazz = cl.loadClass("io.quarkus.test.component.QuarkusComponentFacadeClassLoaderProvider");
857+
Constructor<?> c = qcfcClazz.getConstructor(Class.class, Set.class);
858+
Method getClassLoader = qcfcClazz.getMethod("getClassLoader", String.class, ClassLoader.class);
859+
for (String componentTestClass : quarkusComponentTestClasses) {
860+
try {
861+
Class<?> testClass = cl.loadClass(componentTestClass);
862+
Object ecl = c.newInstance(testClass, classesToTransform);
863+
ClassLoader excl = (ClassLoader) getClassLoader.invoke(ecl, componentTestClass, cl);
864+
utClasses.add(excl.loadClass(componentTestClass));
865+
} catch (Exception e) {
866+
log.debug(e);
867+
log.warnf("Failed to load component test class %s, it will not be executed this run.",
868+
componentTestClass);
869+
}
870+
}
871+
} catch (ClassNotFoundException | IllegalArgumentException
872+
| SecurityException | NoSuchMethodException e) {
873+
log.warn(
874+
"Failed to load QuarkusComponentFacadeClassLoaderProvider, component test classes will not be executed this run.");
875+
}
876+
}
823877
}
824878

825879
if (classLoaderToClose != null) {
@@ -832,6 +886,9 @@ public String apply(Class<?> aClass) {
832886
}
833887
}
834888

889+
// Unset the system property that is used for QuarkusComponentTest
890+
System.clearProperty(TEST_DISCOVERY_PROPERTY);
891+
835892
// Make sure you also update the logic for the empty case above if you adjust this part
836893
if (testType == TestType.ALL) {
837894
//run unit style tests first

integration-tests/devmode/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@
8989
<artifactId>quarkus-junit5-internal</artifactId>
9090
<scope>test</scope>
9191
</dependency>
92+
<!-- quarkus-junit5 provides QuarkusTestConfigProviderResolver
93+
that overrides io.quarkus.test.config.TestConfigProviderResolver
94+
to avoid Context ClassLoader mismatch -->
95+
<dependency>
96+
<groupId>io.quarkus</groupId>
97+
<artifactId>quarkus-junit5</artifactId>
98+
<scope>test</scope>
99+
</dependency>
92100
<dependency>
93101
<groupId>io.quarkus</groupId>
94102
<artifactId>quarkus-junit5-component</artifactId>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.quarkus.test.common;
2+
3+
/**
4+
* This internal SPI is used by {@code io.quarkus.test.junit.classloading.FacadeClassLoader} from quarkus-junit5 to extend its
5+
* functionality.
6+
*/
7+
public interface FacadeClassLoaderProvider {
8+
9+
/**
10+
* @param name The binary name of a class
11+
* @param parent
12+
* @return the class loader or null if no dedicated CL exists for the given class
13+
*/
14+
ClassLoader getClassLoader(String name, ClassLoader parent);
15+
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.quarkus.test.component;
2+
3+
import java.util.Map;
4+
import java.util.Set;
5+
6+
record BuildResult(Map<String, byte[]> generatedClasses,
7+
byte[] componentsProvider,
8+
// prefix -> config mapping FQCN
9+
Map<String, Set<String>> configMappings,
10+
// key -> [testClass, methodName, paramType1, paramType2]
11+
Map<String, String[]> interceptorMethods,
12+
Throwable failure) {
13+
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.quarkus.test.component;
2+
3+
class ComponentClassLoader extends ClassLoader {
4+
5+
private final QuarkusComponentFacadeClassLoaderProvider cls = new QuarkusComponentFacadeClassLoaderProvider();
6+
7+
ComponentClassLoader(ClassLoader parent) {
8+
super(parent);
9+
}
10+
11+
@Override
12+
public Class<?> loadClass(String name) throws ClassNotFoundException {
13+
ClassLoader cl = cls.getClassLoader(name, getParent());
14+
if (cl != null) {
15+
return cl.loadClass(name);
16+
}
17+
return getParent().loadClass(name);
18+
}
19+
20+
}

0 commit comments

Comments
 (0)