spockframework / spock

The Enterprise-ready testing and specification framework.

Home Page:https://spockframework.org

Repository from Github https://github.comspockframework/spockRepository from Github https://github.comspockframework/spock

ByteBuddy-MockMaker uses wrong ClassLoader on Java 21, if mocked type and additional interfaces are from different classloaders

beatbrot opened this issue · comments

Describe the bug

Given the following conditions:

  • ByteBuddy is available
  • We are running on Java 21
  • and the following classloader hierarchy:

Unbenannt-2024-07-14-2000

When mocking type iam.Mocked with additionalInterfaces: [foo.Bar] and if the specification is loaded by MySpecClassLoader, we will get the following exception:

Stacktrace:
java.lang.IllegalArgumentException: Could not create type
	at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:170)
	at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:399)
	at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:190)
	at net.bytebuddy.TypeCache$WithInlineExpunction.findOrInsert(TypeCache.java:410)
	at org.spockframework.mock.runtime.ByteBuddyMockFactory.createMock(ByteBuddyMockFactory.java:97)
	at org.spockframework.mock.runtime.ByteBuddyMockMaker.makeMock(ByteBuddyMockMaker.java:63)
	at org.spockframework.mock.runtime.MockMakerRegistry.createWithAppropriateMockMaker(MockMakerRegistry.java:194)
	at org.spockframework.mock.runtime.MockMakerRegistry.makeMockInternal(MockMakerRegistry.java:153)
	at org.spockframework.mock.runtime.MockMakerRegistry.makeMock(MockMakerRegistry.java:125)
	at org.spockframework.mock.runtime.JavaMockFactory.createInternal(JavaMockFactory.java:56)
	at org.spockframework.mock.runtime.JavaMockFactory.create(JavaMockFactory.java:42)
	at org.spockframework.mock.runtime.CompositeMockFactory.create(CompositeMockFactory.java:44)
	at org.spockframework.lang.SpecInternals.createMock(SpecInternals.java:55)
	at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:330)
	at org.spockframework.lang.SpecInternals.createMockImpl(SpecInternals.java:320)
	at org.spockframework.lang.SpecInternals.MockImpl(SpecInternals.java:123)
	at apackage.ASpec.a feature(Script_2ea152aea2eb5fd1885101de83bd8e60.groovy:1)
Caused by: java.lang.NoClassDefFoundError: foo/Bar
	at java.base/java.lang.System$2.defineClass(System.java:2394)
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1033)
	at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1163)
	at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1639)
	at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:119)
	at net.bytebuddy.dynamic.loading.ClassLoadingStrategy$UsingLookup.load(ClassLoadingStrategy.java:519)
	at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:101)
	at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6367)
	at org.spockframework.mock.runtime.ByteBuddyMockFactory.lambda$createMock$2(ByteBuddyMockFactory.java:137)
	at net.bytebuddy.TypeCache.findOrInsert(TypeCache.java:168)
	... 16 more
Caused by: java.lang.ClassNotFoundException: foo.Bar
	at java.base/java.lang.ClassLoader.findClass(ClassLoader.java:733)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
	... 26 more

Spock will pick the ByteBuddy MockMaker and then use the net.bytebuddy.dynamic.loading.ClassLoadingStrategy.UsingLookup classloading strategy. This strategy receives the lookup of the to-be-mocked class. It then uses that lookup to define the mocked class. Internally, this just means that the classloader of the to-be-mocked is used. In our example, this would be MockedClassClassLoader. This classloader is not able to see foo.Bar and therefore loading the mocked class fails.

To Reproduce

Paste the following testcase in spock.util.EmbeddedSpecRunnerClassLoaderSpec:

  def "ByteBuddy Mocks with interfaces that are not visible to the mocked type's classloader"() {
    def testCl1 = new ByteBuddyTestClassLoader()
    def testCl2 = new ByteBuddyTestClassLoader()
    testCl1.defineInterface("foo.Bar")
    testCl2.defineClass("iam.Mocked")
    def cl = new MultipleParentClassLoader([getClass().classLoader, testCl1, testCl2])
    def runner = new EmbeddedSpecRunner(cl)
    when:
    def r = runner.runFeatureBody("""
def mock = Mock(iam.Mocked, additionalInterfaces: [foo.Bar])
expect:
true
""")
    then:
    noExceptionThrown()
  }

and run gradlew :spock-specs:test --tests "spock.util.EmbeddedSpecRunnerClassLoaderSpec.ByteBuddy Mocks with interfaces that are not visible to the mocked type's classloader" "-Dvariant=4.0" "-DjavaVersion=21"

Expected behavior

The mocking succeeds

Actual behavior

See exception above

Java version

openjdk version "21.0.2" 2024-01-16

Buildtool version

Gradle 8.10.2

What operating system are you using

Windows

Dependencies

Reproducer works in spock repo

Additional context

Mockito has a similar problem. They only use the Lookup strategy for "local mocks" (see here). Local mocks are mocks where the mocked type and all its additional interfaces are loadable by the classloader of the mocked type. I'd suggest implementing the same behavior for Spock and falling back to another loading strategy for non-local mocks. I have a simple version of this change ready and would love to open a PR if we can agree on this solution :)

Also FYI: I discovered this bug together with @AndreasTu

For additional context and as a quick workaround, if you manually select the MockMakers.mockito for the sample above, it will succeed. Showing that the Mockito handling of such a case resolves the issue.

@beatbrot Thanks for the issue report and I would appreciate, if you could open up a PR for that.