Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@ private void ensureDevServicesStarted() {
* Runs the application, and returns a handle that can be used to shut it down.
*/
public RunningQuarkusApplication run(String... args) throws Exception {

if (runtimeClassLoader.isClosed()) {
throw new RuntimeException(
"Internal error: An attempt was made to start an application which had already been started and closed. The affected ClassLoader is "
+ runtimeClassLoader);

}
// Start dev services that weren't started in the augmentation phase
ensureDevServicesStarted();
InitialConfigurator.DELAYED_HANDLER.buildTimeComplete();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void releaseConfig(ShutdownContext shutdownContext) {

if (shutdownContext == null) {
throw new RuntimeException(
"Internal errror: shutdownContext is null. This probably happened because Quarkus failed to start properly in an earlier step, or because tests were run on a Quarkus instance that had already been shut down.");
"Internal error: shutdownContext is null. This probably happened because Quarkus failed to start properly in an earlier step, or because tests were run on a Quarkus instance that had already been shut down.");
}
shutdownContext.addLastShutdownTask(QuarkusConfigFactory::releaseTCCLConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,10 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex
}
cl.getCuratedApplication().setEligibleForReuse(isSameCuratedApplication);

// TODO if classes are misordered, say because someone overrode the ordering, and there are profiles or resources,
// we could try to start and application which has already been started, and fail with a mysterious error about
// null shutdown contexts; we should try and detect that case, and give a friendlier error message
if (cl.isClosed()) {
throw new IllegalStateException(
"Internal error. Attempting to run tests using an application which has already been closed. Is a non-default test order being used?");
}

// We want to start if the profile changed (or there are new test resources),
// or if we don't have an app and that's not because the previous attempt to start it failed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,14 @@ public boolean isServiceLoaderMechanism() {
return isServiceLoaderMechanism;
}

/**
* Returns true if, after test discovery, multiple classloaders have been created. This would mean multiple Quarkus
* applications will be started to run the tests.
*/
public boolean hasMultipleClassLoaders() {
return runtimeClassLoaders != null && runtimeClassLoaders.size() > 1;
}

@Override
public String getName() {
return NAME;
Expand All @@ -589,5 +597,4 @@ public void close() throws IOException {
runtimeClassLoaders.clear();

}

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package io.quarkus.test.junit.launcher;

import java.util.Optional;

import org.jboss.logging.Logger;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;

import io.quarkus.test.config.QuarkusClassOrderer;
import io.quarkus.test.junit.classloading.FacadeClassLoader;

public class CustomLauncherInterceptor
implements LauncherDiscoveryListener, LauncherSessionListener, TestExecutionListener {

private static final Class<? extends ClassOrderer> DESIRED_CLASS_ORDERER = QuarkusClassOrderer.class;
private static final Logger log = Logger.getLogger(CustomLauncherInterceptor.class);

private static FacadeClassLoader facadeLoader = null;
// Also use a static variable to store a 'first' starting state that we can reset to
private static ClassLoader origCl = null;
Expand Down Expand Up @@ -51,7 +59,6 @@ private void actuallyIntercept() {
origCl = Thread.currentThread()
.getContextClassLoader();
}
// We might not be in the same classloader as the Facade ClassLoader, so use a name comparison instead of an instanceof
initializeFacadeClassLoader();
adjustContextClassLoader();

Expand Down Expand Up @@ -118,7 +125,41 @@ public void launcherDiscoveryFinished(LauncherDiscoveryRequest request) {
Thread.currentThread().setContextClassLoader(origCl);

}

Optional<String> orderer = request.getConfigurationParameters().get("junit.jupiter.testclass.order.default");

if (orderer.isEmpty() || !orderer.get().equals(DESIRED_CLASS_ORDERER.getName())) {
if (facadeLoader != null && facadeLoader.hasMultipleClassLoaders()) {
String message = getFailureMessageForJUnitMisconfiguration(orderer);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The orderer, or the facade classloader? (It's a reasonable question for both.)

For the orderer, the JUnit API returns Optional, so I think I can assume it's not null, and I do the isEmpty check.

For FacadeClassLoader, I thought it couldn't be null, but looking more closely, we null it out in launcherSessionClosed and testPlanExecutionFinished. This usage is in launcherDiscoveryFinished. In any normal execution that would always be first, but the JUnit interaction with things like gradle does cause the order to go a bit wonky sometimes. I'll add a null check to be sure.

(We also use the FCL two lines earlier, but only in dev mode.)

// This is likely to be quite a serious problem for tests, but as this is a maintenance stream, be conservative
log.warn(message);
}
}
}
}

private static String getFailureMessageForJUnitMisconfiguration(Optional<String> orderer) {
String generalExplanation = """
Critical problem. Quarkus tests are likely to fail with corrupted application errors.
The reason is that they will not run in the right order, because the Quarkus JUnit configuration has been overridden.
When there are multiple test profiles or resources, Quarkus uses a JUnit ClassOrderer to sort tests so that test with the same profile run one after each other.
Running tests with a different sorting risks tests running on an application that has been cleaned up.
""";

String message;
if (orderer.isPresent()) {
message = String.format(
"%sTo set a test order while preserving the Quarkus required sorting, please use the Quarkus configuration to set junit.quarkus.orderer.secondary-orderer=%s, and remove the junit-platform.properties (if any) from the classpath.",
generalExplanation, orderer.get());
} else {
message = String.format(
"""
%sIt looks like there is a junit-platform.properties configuration file on the project classpath.
The JUnit framework will only read the first properties it finds, which prevents Quarkus from setting the class orderer it needs.
Please either add junit.jupiter.testclass.order.default=%s to your project's junit-platform.properties file, or remove the file and use system properties to configure JUnit.""",
generalExplanation, DESIRED_CLASS_ORDERER.getName());
}
return message;
}

@Override
Expand Down
Loading