Test using LauncherInterceptor fails when executed with Gradle
cdsap opened this issue · comments
In a test using a simple launcher interceptor, test passes if it is executed with the IDE Junit configuration but fails with the IDE Gradle configuration or ./gradlew test
command-line execution. I'm not sure if this issue falls on Junit or Gradle BT. Please, let me know if I should close this issue and create a new one with Gradle.
Having the following LauncherInterceptor:
public class CustomLauncherInterceptor implements LauncherInterceptor {
private final URLClassLoader customClassLoader;
public CustomLauncherInterceptor() throws Exception {
ClassLoader parent = Thread.currentThread().getContextClassLoader();
List<URL> urls = new ArrayList<>();
urls.add(this.getClass().getResource("/"));
customClassLoader = new ExampleClassLoader(urls.toArray(URL[]::new), parent);
}
@Override
public <T> T intercept(Invocation<T> invocation) {
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(customClassLoader);
try {
return invocation.proceed();
}
finally {
currentThread.setContextClassLoader(originalClassLoader);
}
}
...
where the ExampleClassLoader
is:
public class ExampleClassLoader extends URLClassLoader {
public ExampleClassLoader(URL[] urls, ClassLoader classLoader) {
super(urls, classLoader);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, true);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
var ex = findLoadedClass(name);
if (ex != null) {
return ex;
}
var res = getResource(name.replace(".", "/") + ".class");
if (res != null && res.getProtocol().equals("file")) {
try (var in = res.openStream()) {
var data = in.readAllBytes();
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return super.loadClass(name, resolve);
}
}
the expected results for the test:
@Test
public void classLoaderTest() {
String loader = this.getClass().getClassLoader().getClass().getName();
assertThat(loader).isEqualTo("ExampleClassLoader");
}
is passing when executing from the IDE and gradle command:
However, the result with Gradle execution is:
Expected :"ExampleClassLoader"
Actual :"jdk.internal.loader.ClassLoaders$AppClassLoader"
(interceptors has been enabled in the platform properties and CustomLauncherInterceptor registered in the services file)
Steps to reproduce
- Checkout repository https://github.com/cdsap/InterceptorReproducerIssue
- Execute
CustomClassLoaderTest
with Junit Configuration in the IDE. Test passes. - Execute
CustomClassLoaderTest
with Gradle configuration or command-line gradle command. Test fails.
Context
- Used versions (Jupiter/Vintage/Platform): 5.10.2
- junit-platform-launcher: 1.10.2
- Build Tool/IDE: Gradle 8.7 / IDEA
- Build Scans:
- Junit configuration test execution(OK): https://ge.solutions-team.gradle.com/s/nupb64khuzqeq
- Gradle configuration test execution(Fail): https://ge.solutions-team.gradle.com/s/46pkptzlbzj3e
Deliverables
- ...
Hi Iñaki! 🙂 👋
That's caused by Gradle calling selectClass(Class)
after loading the class with the default class loader early to perform some checks.
If Gradle would call selectClass(String)
, it would work. Switching those checks to read the bytecode instead of using reflection would avoid initializing the class potentially multiple times. However, using ASM for that should be avoided since newer JDK require upgrading. The Class-File API would avoid that in the future. I'll leave the decision whether to open an issue in the Gradle repo up to you.
As a workaround, you can replace the ClassLoader
in the constructor of CustomLauncherInterceptor
rather than in intercept()
.