Skip to content

Commit

Permalink
Load tests with runtime classloader, including basic profile support
Browse files Browse the repository at this point in the history
  • Loading branch information
holly-cummins committed Jul 19, 2024
1 parent 07f0957 commit c024bd4
Show file tree
Hide file tree
Showing 40 changed files with 1,746 additions and 264 deletions.

Large diffs are not rendered by default.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.deployment.dev.testing;

import org.jboss.jandex.DotName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;

public final class DotNames {

private DotNames() {
}

public static final DotName EXTEND_WITH = DotName.createSimple(ExtendWith.class.getName());
public static final DotName REGISTER_EXTENSION = DotName.createSimple(RegisterExtension.class.getName());
// TODO this leaks knowledge of the junit5 module into this module
public static final DotName QUARKUS_TEST_EXTENSION = DotName.createSimple("io.quarkus.test.junit.QuarkusTextExtension");
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -22,7 +23,6 @@
import java.util.Set;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -139,9 +139,7 @@ public Runnable prepare() {
LogCapturingOutputFilter logHandler = new LogCapturingOutputFilter(testApplication, true, true,
TestSupport.instance().get()::isDisplayTestOutput);
Thread.currentThread().setContextClassLoader(tcl);
Consumer currentTestAppConsumer = (Consumer) tcl.loadClass(CurrentTestApplication.class.getName())
.getDeclaredConstructor().newInstance();
currentTestAppConsumer.accept(testApplication);
System.out.println("139 HOLLY junit runner set classloader to deployment" + tcl);

Set<UniqueId> allDiscoveredIds = new HashSet<>();
Set<UniqueId> dynamicIds = new HashSet<>();
Expand All @@ -151,6 +149,10 @@ public Runnable prepare() {
LauncherDiscoveryRequestBuilder launchBuilder = LauncherDiscoveryRequestBuilder.request()
.selectors(quarkusTestClasses.testClasses.stream().map(DiscoverySelectors::selectClass)
.collect(Collectors.toList()));

System.out.println("HOLLY in prepare, launch is "
+ quarkusTestClasses.testClasses.stream().map(DiscoverySelectors::selectClass)
.collect(Collectors.toList()));
launchBuilder.filters(new PostDiscoveryFilter() {
@Override
public FilterResult apply(TestDescriptor testDescriptor) {
Expand All @@ -159,6 +161,11 @@ public FilterResult apply(TestDescriptor testDescriptor) {
}
});
if (classScanResult != null) {
System.out.println("HOLLY class scan result is " + classScanResult);
System.out.println("HOLLY changed class names is "
+ classScanResult.getChangedClassNames());
System.out.println("HOLLY filtering is "
+ testClassUsages.getTestsToRun(classScanResult.getChangedClassNames(), testState));
launchBuilder.filters(testClassUsages.getTestsToRun(classScanResult.getChangedClassNames(), testState));
}
if (!includeTags.isEmpty()) {
Expand Down Expand Up @@ -187,6 +194,7 @@ public FilterResult apply(TestDescriptor testDescriptor) {
.build();
TestPlan testPlan = launcher.discover(request);
long toRun = testPlan.countTestIdentifiers(TestIdentifier::isTest);
System.out.println("HOLLY to run is " + toRun);
for (TestRunListener listener : listeners) {
listener.runStarted(toRun);
}
Expand Down Expand Up @@ -223,6 +231,7 @@ public void quarkusStarting() {
AtomicReference<TestIdentifier> currentNonDynamicTest = new AtomicReference<>();

Thread.currentThread().setContextClassLoader(tcl);
System.out.println("224 HOLLY junit runner set classloader to " + tcl);
launcher.execute(testPlan, new TestExecutionListener() {

@Override
Expand All @@ -243,6 +252,7 @@ public void executionStarted(TestIdentifier testIdentifier) {
for (TestRunListener listener : listeners) {
listener.testStarted(testIdentifier, testClassName);
}
System.out.println("HOLLY runner pushing onto touched ");
touchedClasses.push(Collections.synchronizedSet(new HashSet<>()));
}

Expand All @@ -254,6 +264,7 @@ public void executionSkipped(TestIdentifier testIdentifier, String reason) {
touchedClasses.pop();
Class<?> testClass = getTestClassFromSource(testIdentifier.getSource());
String displayName = getDisplayNameFromIdentifier(testIdentifier, testClass);
System.out.println("HOLLY skipping " + displayName);
UniqueId id = UniqueId.parse(testIdentifier.getUniqueId());
if (testClass != null) {
Map<UniqueId, TestResult> results = resultsByClass.computeIfAbsent(testClass.getName(),
Expand Down Expand Up @@ -282,13 +293,18 @@ public void dynamicTestRegistered(TestIdentifier testIdentifier) {
@Override
public void executionFinished(TestIdentifier testIdentifier,
TestExecutionResult testExecutionResult) {
System.out.println("execution finished, " + testExecutionResult);
if (testExecutionResult.getThrowable().isPresent()) {
testExecutionResult.getThrowable().get().printStackTrace();
}
if (aborted) {
return;
}
boolean dynamic = dynamicIds.contains(UniqueId.parse(testIdentifier.getUniqueId()));
Set<String> touched = touchedClasses.pop();
Class<?> testClass = getTestClassFromSource(testIdentifier.getSource());
String displayName = getDisplayNameFromIdentifier(testIdentifier, testClass);
System.out.println("execution finished display name was " + displayName);
UniqueId id = UniqueId.parse(testIdentifier.getUniqueId());

if (testClass == null) {
Expand Down Expand Up @@ -391,10 +407,10 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e
}
} finally {
try {
currentTestAppConsumer.accept(null);
TracingHandler.setTracingHandler(null);
QuarkusConsole.removeOutputFilter(logHandler);
Thread.currentThread().setContextClassLoader(old);
System.out.println("398 HOLLY junit runner set classloader to old " + old);
tcl.close();
try {
quarkusTestClasses.close();
Expand All @@ -403,6 +419,7 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e
}
} finally {
Thread.currentThread().setContextClassLoader(origCl);
System.out.println("406 HOLLY junit runner set classloader to orig " + origCl);
synchronized (JunitTestRunner.this) {
testsRunning = false;
if (aborted) {
Expand Down Expand Up @@ -528,11 +545,14 @@ private Map<String, TestClassResult> toResultsMap(
}

private DiscoveryResult discoverTestClasses() {
System.out.println(new Date() + "533 HOLLY doing discovery");
//maven has a lot of rules around this and is configurable
//for now this is out of scope, we are just going to do annotation based discovery
//we will need to fix this sooner rather than later though

//we also only run tests from the current module, which we can also revisit later

// TODO consolidate logic here with facadeclassloader, which is trying to solve similar problems; maybe even share the canary loader class?
Indexer indexer = new Indexer();
moduleInfo.getTest().ifPresent(test -> {
try (Stream<Path> files = Files.walk(Paths.get(test.getClassesPath()))) {
Expand All @@ -549,6 +569,8 @@ private DiscoveryResult discoverTestClasses() {
});

Index index = indexer.complete();

System.out.println("HOLLY index found known classes " + Arrays.toString(index.getKnownClasses().toArray()));
//we now have all the classes by name
//these tests we never run
Set<String> integrationTestClasses = new HashSet<>();
Expand Down Expand Up @@ -598,6 +620,9 @@ private DiscoveryResult discoverTestClasses() {
}
}
}
System.out.println("HOLLY all test classes is " + Arrays.toString(allTestClasses.toArray()));
System.out.println("HOLLY quarkus test classes is " + Arrays.toString(quarkusTestClasses.toArray()));
System.out.println("HOLLY integration classes is " + Arrays.toString(integrationTestClasses.toArray()));
//now we have all the classes with @Test
//figure out which ones we want to actually run
Set<String> unitTestClasses = new HashSet<>();
Expand Down Expand Up @@ -626,13 +651,42 @@ private DiscoveryResult discoverTestClasses() {

List<Class<?>> itClasses = new ArrayList<>();
List<Class<?>> utClasses = new ArrayList<>();

// TODO batch by profile and start once for each profile

// TODO guard to only do this once? is this guard sufficient? see "wrongprofile" in QuarkusTestExtension

System.out.println(
"HOLLY after the re-add or whatever? quarkus test classes is " + Arrays.toString(quarkusTestClasses.toArray()));
ClassLoader rcl = null;
System.out.println("classload thread is " + Thread.currentThread());

for (String i : quarkusTestClasses) {
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
if (rcl == null) {
System.out.println("HOLLY Making a java start with " + testApplication);
// Although it looks like we need to start once per class, the class is just indicative of where classes for this module live

// CuratedApplications cannot (right now) be re-used between restarts. So even though the builder gave us a
// curated application, don't use it.
// TODO can we make the app re-usable, or otherwise leverage the app that we got passed in?
// TODO sort out profiles!
rcl = new AppMakerHelper()
.getStartupAction(Thread.currentThread().getContextClassLoader().loadClass(i), null,
true, null);
}
Thread.currentThread().setContextClassLoader(rcl);

System.out.println("639 HOLLY loading quarkus test with " + Thread.currentThread().getContextClassLoader());
itClasses.add(Thread.currentThread().getContextClassLoader().loadClass(i));
} catch (ClassNotFoundException e) {
} catch (Exception e) {
System.out.println("HOLLY BAD BAD" + e);
log.warnf(
"Failed to load test class %s (possibly as it was added after the test run started), it will not be executed this run.",
i);
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}
itClasses.sort(Comparator.comparing(new Function<Class<?>, String>() {
Expand All @@ -647,12 +701,15 @@ public String apply(Class<?> aClass) {
}
}));
QuarkusClassLoader cl = null;
System.out.println("HOLLY made unit test classes " + Arrays.toString(unitTestClasses.toArray()));
if (!unitTestClasses.isEmpty()) {
//we need to work the unit test magic
//this is a lot more complex
//we need to transform the classes to make the tracing magic work
QuarkusClassLoader deploymentClassLoader = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader();
System.out.println("HOLLY asking classloader " + deploymentClassLoader);
Set<String> classesToTransform = new HashSet<>(deploymentClassLoader.getLocalClassNames());
System.out.println("HOLLY to transform is " + Arrays.toString(classesToTransform.toArray()));
Map<String, byte[]> transformedClasses = new HashMap<>();
for (String i : classesToTransform) {
try {
Expand All @@ -672,6 +729,7 @@ public String apply(Class<?> aClass) {
cl.reset(Collections.emptyMap(), transformedClasses);
for (String i : unitTestClasses) {
try {
System.out.println("678 HOLLY loaded " + i + " with loader " + cl);
utClasses.add(cl.loadClass(i));
} catch (ClassNotFoundException exception) {
log.warnf(
Expand All @@ -681,13 +739,15 @@ public String apply(Class<?> aClass) {
}

}
System.out.println("HOLLY test tyoe us " + testType);
if (testType == TestType.ALL) {
//run unit style tests first
//before the quarkus tests have started
//which stops quarkus interfering with WireMock
List<Class<?>> ret = new ArrayList<>(utClasses.size() + itClasses.size());
ret.addAll(utClasses);
ret.addAll(itClasses);
System.out.println("RETURNING " + Arrays.toString(ret.toArray()));
return new DiscoveryResult(cl, ret);
} else if (testType == TestType.UNIT) {
return new DiscoveryResult(cl, utClasses);
Expand Down Expand Up @@ -780,6 +840,7 @@ public Builder setTestType(TestType testType) {
return this;
}

// TODO we now ignore what gets set here and make our own, how to handle that?
public Builder setTestApplication(CuratedApplication testApplication) {
this.testApplication = testApplication;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.test.common;
package io.quarkus.deployment.dev.testing;

import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation;

Expand All @@ -22,6 +22,7 @@
import org.jboss.jandex.UnsupportedVersion;

import io.quarkus.fs.util.ZipUtils;
import io.quarkus.test.common.PathTestHelper;

public final class TestClassIndexer {

Expand Down Expand Up @@ -68,6 +69,10 @@ public static Index readIndex(Class<?> testClass) {
return readIndex(getTestClassesLocation(testClass), testClass);
}

public static Index readIndex(Path testLocation) {
return indexTestClasses(testLocation);
}

public static Index readIndex(Path testClassLocation, Class<?> testClass) {
Path path = indexPath(testClassLocation, testClass);
if (path.toFile().exists()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.quarkus.test.common;
package io.quarkus.deployment.dev.testing;

public class TestStatus {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,40 @@ public void close() throws Exception {

@Override
public <T> Optional<T> getConfigValue(String key, Class<T> type) {
//the config is in an isolated CL
//we need to extract it via reflection
//this is pretty yuck, but I don't really see a solution
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Class<?> configProviderClass = classLoader.loadClass(ConfigProvider.class.getName());
Method getConfig = configProviderClass.getMethod("getConfig", ClassLoader.class);
Thread.currentThread().setContextClassLoader(classLoader);
Object config = getConfig.invoke(null, classLoader);
return (Optional<T>) getConfig.getReturnType().getMethod("getOptionalValue", String.class, Class.class)
.invoke(config, key, type);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
Thread.currentThread().setContextClassLoader(old);

if (false && new Exception().getStackTrace().length > 100) {
// TODO expensive, awkward
System.out.println("HOLLY averting infinite loop " + key);
new Exception().printStackTrace();
return Optional.empty();
} else {
ClassLoader old = Thread.currentThread()
.getContextClassLoader();
try {
// TODO this infinite loops, check that assumption
// we are assuming here that the the classloader has been initialised with some kind of different provider that does not infinite loop.
Thread.currentThread()
.setContextClassLoader(classLoader);
if (classLoader == ConfigProvider.class.getClassLoader()) {
return ConfigProvider.getConfig(classLoader)
.getOptionalValue(key, type);
} else {
//the config is in an isolated CL
//we need to extract it via reflection
//this is pretty yuck, but I don't really see a solution
Class<?> configProviderClass = classLoader.loadClass(ConfigProvider.class.getName());
Method getConfig = configProviderClass.getMethod("getConfig", ClassLoader.class);
Object config = getConfig.invoke(null, classLoader);
return (Optional<T>) getConfig.getReturnType()
.getMethod("getOptionalValue", String.class, Class.class)
.invoke(config, key, type);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
Thread.currentThread()
.setContextClassLoader(old);
}
}
}

Expand Down
Loading

0 comments on commit c024bd4

Please sign in to comment.