Skip to content

Commit 8a3ae8c

Browse files
committed
Ensure FacadeClassLoader is not set as the TCCL during continuous test execution, initialise config on the CL created by JUnitTestRunner
1 parent 28074c2 commit 8a3ae8c

File tree

5 files changed

+47
-16
lines changed

5 files changed

+47
-16
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public class JunitTestRunner {
9898
public static final DotName TESTABLE = DotName.createSimple(Testable.class.getName());
9999
public static final DotName NESTED = DotName.createSimple(Nested.class.getName());
100100
private static final String ARCHUNIT_FIELDSOURCE_FQCN = "com.tngtech.archunit.junit.FieldSource";
101-
public static final String FACADE_CLASS_LOADER_NAME = "io.quarkus.test.junit.classloading.FacadeClassLoader";
101+
private static final String FACADE_CLASS_LOADER_NAME = "io.quarkus.test.junit.classloading.FacadeClassLoader";
102102

103103
private final long runId;
104104
private final DevModeContext.ModuleInfo moduleInfo;

integration-tests/command-mode/src/test/java/org/acme/MainContinuousTestingTest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@
44
import org.jboss.shrinkwrap.api.asset.StringAsset;
55
import org.jboss.shrinkwrap.api.spec.JavaArchive;
66
import org.junit.jupiter.api.Assertions;
7-
import org.junit.jupiter.api.Disabled;
87
import org.junit.jupiter.api.Test;
98
import org.junit.jupiter.api.extension.RegisterExtension;
109

1110
import io.quarkus.test.ContinuousTestingTestUtils;
1211
import io.quarkus.test.ContinuousTestingTestUtils.TestStatus;
1312
import io.quarkus.test.QuarkusDevModeTest;
1413

15-
@Disabled("See https://github.com/quarkusio/quarkus/issues/49780#issuecomment-3265067545")
1614
public class MainContinuousTestingTest {
1715
@RegisterExtension
1816
final static QuarkusDevModeTest TEST = new QuarkusDevModeTest()
1917
.withApplicationRoot(jar -> jar
20-
.addClass(Main.class)
18+
// Files from this module are added automatically, so no need to add Main.class
2119
.add(new StringAsset(ContinuousTestingTestUtils.appProperties()), "application.properties"))
2220
.setTestArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
2321
.addClass(MainTest.class));

test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con
139139
return ConditionEvaluationResult.enabled("Quarkus Test Profile tags only affect classes");
140140
}
141141

142-
// At this point, the TCCL is usually the FacadeClassLoader, but sometimes it's a deployment classloader (for multimodule tests), or the runtime classloader (for nested tests)
143-
// Getting back to the FacadeClassLoader is non-trivial. We can't use the singleton on the class, because we will be accessing it from different classloaders.
142+
// At this point, the TCCL is sometimes a deployment classloader (for multimodule tests), or the runtime classloader (for nested tests), and sometimes a FacadeClassLoader in continuous cases
143+
// Getting back to a FacadeClassLoader is non-trivial. We can't use the singleton on the class, because we will be accessing it from different classloaders.
144144
// We can't have a hook back from the runtime classloader to the facade classloader, because
145145
// when evaluating execution conditions for native tests, the test will have been loaded with the system classloader, not the runtime classloader.
146146
// The one classloader we can reliably get to when evaluating test execution is the system classloader, so hook our config on that.

test-framework/junit5/src/main/java/io/quarkus/test/junit/classloading/FacadeClassLoader.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ public final class FacadeClassLoader extends ClassLoader implements Closeable {
113113
private final boolean isAuxiliaryApplication;
114114
private QuarkusClassLoader keyMakerClassLoader;
115115

116+
private final boolean isContinuousTesting;
117+
116118
public FacadeClassLoader(ClassLoader parent) {
117119
this(parent, false, null, null, null, System.getProperty("java.class.path"));
118120
}
@@ -122,9 +124,19 @@ public FacadeClassLoader(ClassLoader parent, boolean isAuxiliaryApplication, Cur
122124
final Map<String, String> profileNames,
123125
final Set<String> quarkusTestClasses, final String classesPath) {
124126
super(parent);
127+
// Note that in normal testing, the parent is the system classloader, and in continuous testing, the parent is a quarkus classloader
128+
// It would be nice to resolve that inconsistency, but I'm not sure it's very possible
125129

126-
// Don't make a no-profile curated application, since our caller had one already
127-
curatedApplications.put(getProfileKey(null), curatedApplication);
130+
if (quarkusTestClasses != null) {
131+
isContinuousTesting = true;
132+
} else {
133+
isContinuousTesting = false;
134+
}
135+
136+
// Don't make a no-profile curated application if our caller had one already
137+
if (curatedApplication != null) {
138+
curatedApplications.put(getProfileKey(null), curatedApplication);
139+
}
128140

129141
this.quarkusTestClasses = quarkusTestClasses;
130142
this.isAuxiliaryApplication = isAuxiliaryApplication;
@@ -227,6 +239,20 @@ public FacadeClassLoader(ClassLoader parent, boolean isAuxiliaryApplication, Cur
227239
// We set it to null so we know not to look in it
228240
this.profiles = null;
229241
}
242+
243+
// In the case where a QuarkusMainTest is present, but not a QuarkusTest, tests will be loaded and run using
244+
// the parent classloader. In continuous testing mode, that will be a Quarkus classloader and it will not have
245+
// the test config on it, so get that classloader test-ready.
246+
247+
// It would be nice to do this without the guard, but doing this on the normal path causes us to write something we don't want
248+
if (isContinuousTesting) {
249+
try {
250+
initialiseTestConfig(parent);
251+
} catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | InstantiationException
252+
| IllegalAccessException e) {
253+
throw new RuntimeException(e);
254+
}
255+
}
230256
}
231257

232258
@Override
@@ -254,7 +280,7 @@ public Class<?> loadClass(String name) throws ClassNotFoundException {
254280
try {
255281

256282
Class<?> profile = null;
257-
if (profiles != null && !isServiceLoaderMechanism) {
283+
if (isContinuousTesting && !isServiceLoaderMechanism) {
258284
isQuarkusTest = quarkusTestClasses.contains(name);
259285

260286
profile = profiles.get(name);
@@ -551,6 +577,14 @@ private QuarkusClassLoader getOrCreateRuntimeClassLoader(String key, Class<?> re
551577
// If the try block fails, this would be null, but there's no catch, so we'd never get to this code
552578
QuarkusClassLoader loader = startupAction.getClassLoader();
553579

580+
initialiseTestConfig(loader);
581+
582+
return loader;
583+
584+
}
585+
586+
private static void initialiseTestConfig(ClassLoader loader) throws ClassNotFoundException, InstantiationException,
587+
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
554588
// Make sure that our new classloader has config on it; this is a bit of a scattergun approach to setting config, but it helps cover most paths
555589
Class<?> configProviderResolverClass = loader.loadClass(ConfigProviderResolver.class.getName());
556590

@@ -560,9 +594,6 @@ private QuarkusClassLoader getOrCreateRuntimeClassLoader(String key, Class<?> re
560594

561595
configProviderResolverClass.getDeclaredMethod("setInstance", configProviderResolverClass)
562596
.invoke(null, testConfigProviderResolver);
563-
564-
return loader;
565-
566597
}
567598

568599
public boolean isServiceLoaderMechanism() {

test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/CustomLauncherInterceptor.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ public void launcherSessionOpened(LauncherSession session) {
3131
* However, the Eclipse runner calls this twice, and the second invocation happens after discovery,
3232
* which means there is no one to unset the TCCL. That breaks integration tests, so we
3333
* need to add an ugly guard to not adjust the TCCL the second time round in that scenario.
34+
*
35+
* There is a similar issue with continuous testing, causing us to go into tests with the FacadeClassLoader set.
36+
* That's not the right CL, so in those cases skip setting it.
37+
*
3438
* We do not do any classloading dance for prod mode tests.
3539
*/
36-
boolean isEclipse = System.getProperty("sun.java.command") != null
37-
&& System.getProperty("sun.java.command").contains("JUnit5TestLoader");
38-
boolean shouldSkipSettingTCCL = isEclipse && discoveryStarted;
40+
boolean shouldSetTCCL = !discoveryStarted;
3941

40-
if (!isProductionModeTests() && !shouldSkipSettingTCCL) {
42+
if (!isProductionModeTests() && shouldSetTCCL) {
4143
actuallyIntercept();
4244
}
4345

0 commit comments

Comments
 (0)