junit-team / junit5

✅ The 5th major version of the programmer-friendly testing framework for Java and the JVM
https://junit.org
Eclipse Public License 2.0
6.44k stars 1.5k forks source link

Test using LauncherInterceptor fails when executed with Gradle #3746

Closed cdsap closed 8 months ago

cdsap commented 8 months ago

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:

Screenshot 2024-03-25 at 9 56 01 AM

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

  1. Checkout repository https://github.com/cdsap/InterceptorReproducerIssue
  2. Execute CustomClassLoaderTest with Junit Configuration in the IDE. Test passes.
  3. Execute CustomClassLoaderTest with Gradle configuration or command-line gradle command. Test fails.

Context

Deliverables

marcphilipp commented 8 months ago

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().