8000 Mocking jOOQ 3.15+ fails with "attempted to add a method" · Issue #1118 · mockk/mockk · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Mocking jOOQ 3.15+ fails with "attempted to add a method" #1118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
3 tasks done
marcelstoer opened this issue Jul 12, 2023 · 27 comments
Closed
3 tasks done

Mocking jOOQ 3.15+ fails with "attempted to add a method" #1118

marcelstoer opened this issue Jul 12, 2023 · 27 comments

Comments

@marcelstoer
Copy link
Contributor

This issue is the continuation of the analysis started and documented at https://stackoverflow.com/q/76665966/131929. Also, this is very closely related to #255 and #477 which both became stale.

I tried to debug further but the exception stems from native JVM code. Hence, I suspect it requires someone intimately familiar with this project to look into it. Allow me to tag the top contributors @oleksiyp, @aSemy and @Raibaz.

Prerequisites

  • I am running the latest version
  • I checked the documentation and found no answer
  • I checked to make sure that this issue has not already been filed

Expected Behavior

Mocking the jOOQ 3.15+ TableImpl class should work.

Current Behavior

It fails with

java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

Failure Information (for bugs)

Mocking the same class with jOOQ 3.14 works fine. The 3.15+ versions are different in that they also implement interfaces which seems to trip up MockK. With the earlier jOOQ versions MockK prints this trace log

Taking instance of class org.jooq.impl.TableImpl itself because it is not abstract and no additional interfaces specified.

It fails both on Java 11 and Java 17.

Steps to Reproduce

Check out the reproducer at https://github.com/marcelstoer/jooq-mockk-test and run mvn test.

@aSemy
Copy link
Contributor
aSemy commented Jul 12, 2023

I've had a quick look, and though I don't think I can help (I don't have much time, and I haven't worked on any projects that use MockK in a long time), I will summarise in case it is useful.

The MockK log message,

[io.mockk.proxy.common.ProxyMaker] (main) Taking instance of class org.jooq.impl.TableImpl itself bec
8000
ause it is not abstract and no additional interfaces specified.

originates from ProxyMaker.kt:

private fun <T : Any> subclass(
clazz: Class<T>,
interfaces: Array<Class<*>>
): Class<T> {
return if (Modifier.isFinal(clazz.modifiers)) {
log.trace("Taking instance of $clazz itself because it is final.")
clazz
} else if (interfaces.isEmpty() && !Modifier.isAbstract(clazz.modifiers) && inliner != null) {
log.trace("Taking instance of $clazz itself because it is not abstract and no additional interfaces specified.")
clazz
} else {
log.trace(
"Building subclass proxy for $clazz with " +
"additional interfaces ${interfaces.toList()}"
)
subclasser.subclass(clazz, interfaces)
}
}

MockK uses ByteBuddy for instantiating new subclasses, and that's where the attempted to add a method error comes from.

This SO post seems to indicate that MockK is using Byte Buddy incorrectly https://stackoverflow.com/questions/40774684/, but it could be something else.

I suspect that this is the MockK function that needs further inspection:

private fun <T> doInterceptedSubclassing(
clazz: Class<T>,
interfaces: Array<Class<*>>
): Class<out T> {
val resultClassLoader = MultipleParentClassLoader.Builder()
.append(clazz)
.append(*interfaces)
.append(currentThread().contextClassLoader)
.append(JvmMockKProxyInterceptor::class.java)
.build(JvmMockKProxyInterceptor::class.java.classLoader)
val interceptor = MethodDelegation.withDefaultConfiguration()
.withBinders(
TargetMethodAnnotationDrivenBinder.ParameterBinder.ForFixedValue.OfConstant.of(
ProxyAdviceId::class.java, interceptor.id
)
)
.to(JvmMockKProxyInterceptor::class.java)
val type = byteBuddy.subclass(clazz)
.implement(*interfaces)
.annotateType(*clazz.annotations)
.method(any<Any>())
.intercept(interceptor)
.attribute(MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER)
.make()
try {
val property = System.getProperty("io.mockk.classdump.path")
if (property != null) {
val nextIndex = classDumpIndex.incrementAndGet().toString()
val storePath = File(File(property, "subclass"), nextIndex)
type.saveIn(storePath)
}
} catch (ex: Exception) {
log.trace(ex, "Failed to save file to a dump")
}
return type
.load(resultClassLoader, chooseClassLoadingStrategy(clazz))
.loaded
}

I would encourage you, or anyone else facing this issue, to try poking around in the MockK code. Make a PR that adds your test from https://github.com/marcelstoer/jooq-mockk-test into
the test-modules/client-tests subproject, and then try experimenting. Personally I found MockK quite daunting until I started making some PRs, and then I realised it's much less complicated that I first thought, and @Raibaz is a really nice maintainer!

@marcelstoer
Copy link
Contributor Author

Thank you @aSemy for the extra documentation. I had gone through all that code before filing this issue (that's how I found #1117 😄 ) but I missed to add pointers to it my description, sorry.

Interesting SO reference. Confirming that an established library like MockK is using its core foundation incorrectly would be quite a thing I guess.

@marcelstoer
Copy link
Contributor Author
marcelstoer commented May 29, 2024

I just found another such case in a different library.

@lukehutch's classgraph library cannot be updated beyond 4.8.64 (from early 2020). Starting from 4.8.65 I am getting the dreaded "class redefinition failed: attempted to add a method" error. Since the exception is triggered in native code, I can't pinpoint which exact class causes this. However, I think I can limit the set of potential culprits to four classes - all in the same inheritance hierarchy. See below

Screenshot 2024-05-29 at 13 16 10

Yet, when I compare the changes that went into that release, I see nothing that would affect the class signatures in any way.

--> I'm as clueless as before

@marcelstoer
Copy link
Contributor Author
marcelstoer commented Nov 5, 2024

Btw, the unit test in #1319 demonstrates that the stacktrace is as follows.

java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method
	at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
	at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:169)
	at io.mockk.proxy.jvm.transformation.JvmInlineInstrumentation.retransform(JvmInlineInstrumentation.kt:28)
	at io.mockk.proxy.common.transformation.RetransformInlineInstrumentation$execute$1.invoke(RetransformInlineInstrumentation.kt:19)
	at io.mockk.proxy.common.transformation.RetransformInlineInstrumentation$execute$1.invoke(RetransformInlineInstrumentation.kt:16)
	at io.mockk.proxy.common.transformation.ClassTransformationSpecMap.applyTransformation(ClassTransformationSpecMap.kt:41)
	at io.mockk.proxy.common.transformation.RetransformInlineInstrumentation.execute(RetransformInlineInstrumentation.kt:16)
	at io.mockk.proxy.jvm.ProxyMaker.inline(ProxyMaker.kt:90)
	at io.mockk.proxy.jvm.ProxyMaker.proxy(ProxyMaker.kt:34)
	at io.mockk.impl.instantiation.JvmMockFactory.newProxy(JvmMockFactory.kt:34)
	at io.mockk.impl.instantiation.AbstractMockFactory.newProxy$default(AbstractMockFactory.kt:24)
	at io.mockk.impl.instantiation.AbstractMockFactory.mockk(AbstractMockFactory.kt:59)
	at io.mockk.impl.jooq.JooqTest.should mock jOOQ TableImpl class(JooqTest.kt:26)

I started digging at ProxyMaker#inline().

@aSemy Side note, JvmSubclassInstrumentation#doInterceptedSubclassing() is not involved here as ProxyMaker#proxy fails with inline() i.e. before it even gets to subclass at

val cancellation = inline(actualClass)
val result = CancelableResult<T>(cancelBlock = cancellation)
val proxyClass = try {
subclass(actualClass, interfaces)
} catch (ex: Exception) {
result.cancel()
throw MockKAgentException("Failed to subclass $actualClass", ex)
}

The other thing I discovered is that the call stack when running the test against jOOQ 3.14.16 (passes) is exactly identical to that of running against 3.19.14 (fails). Not sure though what this means, good or bad? Should it be different because in the later case the test class implements a pile of interfaces?

marcelstoer added a commit to marcelstoer/mockk that referenced this issue Nov 6, 2024
@marcelstoer
Copy link
Contributor Author
marcelstoer commented Nov 6, 2024

Ok, a smoking gun at last - maybe. @Raibaz I'd need some guidance on this.

I noticed that, contrary to its name, ProxyMaker.Companion#getAllSuperclasses(Class) returns all the parameter's super classes (and interfaces) plus itself. That didn't feel right. It would also explain why in some situations a class may be manipulated twice, once as a dedicated class and once as part of the superclasses collection.

So, I changed the implementation such that it only returns super classes (and interfaces) like so:
Screenshot 2024-11-06 at 11 38 14
(I guess you could say it's now post-order traversal rather than pre-order traversal.)

This fixes it for my use case but other tests started failing.

@Raibaz
Copy link
Collaborator
Raibaz commented Nov 6, 2024

Ah, this is interesting indeed!

What are the tests that started failing? The way it looks, the correct behavior is that getAllSuperclasses(Class) should not return the class itself, so it may be the case that we need to update the other test cases.

@marcelstoer
Copy link
Contributor Author

io.mockk.test.BasicClientTest#aVeryBasicClientTest for example fails with

Failed matching mocking signature for

left matchers: [any()]
io.mockk.MockKException: Failed matching mocking signature for

left matchers: [any()]
	at io.mockk.impl.recording.SignatureMatcherDetector.detect(SignatureMatcherDetector.kt:97)
	at io.mockk.impl.recording.states.RecordingState.signMatchers(RecordingState.kt:39)
	at io.mockk.impl.recording.states.RecordingState.round(RecordingState.kt:31)
	at io.mockk.impl.recording.CommonCallRecorder.round(CommonCallRecorder.kt:50)
	at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:62)
	at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
	at io.mockk.MockKDsl.internalEvery(API.kt:94)
	at io.mockk.MockKKt.every(MockK.kt:143)
	at io.mockk.test.BasicClientTest.aVeryBasicClientTest(BasicClientTest.kt:22)

As this is something rather serious, I suspect that other code relies on getAllSuperclasses() returning not just the super classes as its name suggests.

I'll commit my fix to PR #1319 anyway and would appreciate if you could also take a look, thanks.

marcelstoer added a commit to marcelstoer/mockk that referenced this issue Jan 15, 2025
@marcelstoer
Copy link
Contributor Author
marcelstoer commented Jan 15, 2025

@Raibaz I rebased my test branch onto master and spent a few more hours on this today. However, with my limited understanding of how MockK works this just leads nowhere, sorry.

After all, I think getAllSuperclasses(Class) is required to behave the way it currently behaves. It likely just has a subpar name. I traced the origins of that code all the way back to November 2017 when @oleksiyp first added it with 1c322c7 (io.mockk.proxy.MockKProxyMaker#getAllSuperclasses(Class)). Hence, it always returned the superclasses and the class itself. It should be called something like getAllSuperclassesAndItself.

As for the actual issue, I'm more puzzled then ever before. io.mockk.proxy.jvm.transformation.JvmInlineInstrumentation#retransform considers all classes as isModifiableClass. Yet, when retransformClasses is actually called on them, it fails.

Screenshot 2025-01-15 at 14 05 26

@Raibaz If you have a minute, I would very much appreciate if you could debug my tiny test in #1319 and see if you spot anything obvious.

@Raibaz
Copy link
Collaborator
Raibaz commented Jan 15, 2025

I'm a bit puzzled myself because that's the ByteBuddy part in MockK, that I'm not really familiar with.

May be worth getting in touch with the ByteBuddy team to see if they have any pointers.

@marcelstoer
Copy link
Contributor Author
marcelstoer commented Jan 16, 2025

@aSemy had already dug up an SO post that suggested MockK used ByteBuddy incorrectly. As I am not familiar with ByteBuddy either, I can't comment on that.

This SO post seems to indicate that MockK is using Byte Buddy incorrectly https://stackoverflow.com/questions/40774684/, but it could be something else.

However, AFAIU that centered around the way subclasses are built. If you execute my test, it looks like it fails even before the subclassing step. So, this is maybe the wrong lead entirely.

@sgerke-1L
Copy link
Contributor

We're running into this as well, is there any additional information?

@bdalenoord
Copy link

Hm, we're running into some issues with this as well, specifically when we're mocking jOOQ-codegen based records since jOOQ 3.20.

After digging into their changes a bit only one change is visible in the list of classes within JvmInlineInstrumentation: a new interface (FieldsTrait) was added as an additional interface for the generated class being mocked/causing the failure.

This interface differs from the others that were already on the class being mocked in that it has no public accessibility modifier, making it module-private.

I have however not been able to reproduce anything similar when building up a minimal sample thusfar. I may dig into it a bit more tomorrow, as this prevents us from updating jOOQ atm, which would become an issue in the near future.

@marcelstoer
Copy link
Contributor Author

@bdalenoord maybe it helps if you start with the test I added in #1319.

@bdalenoord
Copy link

@marcelstoer strange thing is that our project succeeds with jOOQ 3.19.20 😕 but maybe I can figure something out with your testcase indeed, thx for the pointer.

@bdalenoord
Copy link
bdalenoord commented Mar 25, 2025

@marcelstoer I've checked out your branch from #1319, and your test fails succesfully there. I've also added a test-case to simulate our situation:

    @Test
    fun `should mock jOOQ UpdatableRecordImpl class`() {
        val updateableRecord = mockk<UpdatableRecordImpl<*>>()
        val record = mockk<Record>()
        every { updateableRecord.key() } returns record
        assertEquals(record, updateableRecord.key())
    }

where instead of mocking a TableImpl, I've mocked a UpdateableRecordImpl. That test succeeds with jOOQ version 3.19.14.

When I update jOOQ to 3.20.2 (the latest, the version that I was trying to update our project to), suddenly, both testcases fail.

As I concluded yesterday, the only difference for UpdateableRecordImpl is that a package-private FieldsTrait-interface has been added, so I'm gonna dig in that direction a bit further.

Edit hmm. Simply excluding package-private interfaces from the ProxyMaker#addInterfaces does not fix the test, whilst in that case the exact same list of classes/interfaces is being considered within the JvmInlineInstrumentation as there was in 3.19. I'm losing leads and don't really have an idea how to continue investigation.

@marcelstoer
Copy link
Contributor Author

First things first, do you get the same exception as in my documented case?

java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

If so, can you debug to reproduce the same behavior I documented at #1118 (comment)? All classes considered modifiable, yet the retransformation then still fails?

@Raibaz

May be worth getting in touch with the ByteBuddy team to see if they have any pointers.

Can we ask you to do that (contact user 'raphw')? I am certain it promises better results if they are contacted by the maintainer of a reputable project than by some random dudes.

@bdalenoord
Copy link

@marcelstoer the exception and top of the stacktrace are exactly the same for both the Table-based as well as the Record-based mock once I switch to jOOQ 3.20.2, and I can confirm that all classes are considered modifiable before attempting retransformation:

Image

@sgerke-1L
Copy link
Contributor

Do we know which method it tries do add to which class and then fails?

@bdalenoord
Copy link

@sgerke-1L I don't, and as the error happens in native code I'm not sure how I could be able to figure that out, no way to inspect the runtime at that point.

@bdalenoord
Copy link
bdalenoord commented Mar 26, 2025

Digging around with Unified Logging I've got some insight into where the failure occurs. I've added -Xlog:redefine+class*=trace:stdout as JVM-arg to obtain these.

Logging for the Record-based test-case (my reproduction that fails with 3.20 but succeeds with 3.19):

[1,523s][debug][redefine,class,load] loading name=org.jooq.impl.UpdatableRecordImpl kind=101 (avail_mem=2819128K)
[1,590s][trace][redefine,class,iklass,purge] Class unloading: should_clean_previous_versions = false
[1,951s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.refresh([Lorg/jooq/Field;)V [0] == old: org.jooq.impl.UpdatableRecordImpl.refresh([Lorg/jooq/Field;)V [0]
[1,951s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.refresh(Ljava/util/Collection;)V [1] == old: org.jooq.impl.UpdatableRecordImpl.refresh(Ljava/util/Collection;)V [1]
[1,951s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.refresh()V [2] == old: org.jooq.impl.UpdatableRecordImpl.refresh()V [2]
[1,951s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.<init>(Lorg/jooq/Table;)V [3] == old: org.jooq.impl.UpdatableRecordImpl.<init>(Lorg/jooq/Table;)V [3]
[1,951s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.<clinit>()V [4] == old: org.jooq.impl.UpdatableRecordImpl.<clinit>()V [4]
[1,951s][info ][redefine,class,normalize   ] redefined class org.jooq.impl.UpdatableRecordImpl methods error: added method: org.jooq.impl.UpdatableRecordImpl.type(Lorg/jooq/Name;)Ljava/lang/Class; [5]
[1,957s][debug][redefine,class,load        ] loading name=org.jooq.impl.UpdatableRecordImpl kind=101 (avail_mem=2945516K)
[1,985s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.refresh([Lorg/jooq/Field;)V [0] == old: org.jooq.impl.UpdatableRecordImpl.refresh([Lorg/jooq/Field;)V [0]
[1,985s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.refresh(Ljava/util/Collection;)V [1] == old: org.jooq.impl.UpdatableRecordImpl.refresh(Ljava/util/Collection;)V [1]
[1,985s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.refresh()V [2] == old: org.jooq.impl.UpdatableRecordImpl.refresh()V [2]
[1,985s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.<init>(Lorg/jooq/Table;)V [3] == old: org.jooq.impl.UpdatableRecordImpl.<init>(Lorg/jooq/Table;)V [3]
[1,985s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.UpdatableRecordImpl.<clinit>()V [4] == old: org.jooq.impl.UpdatableRecordImpl.<clinit>()V [4]
[1,985s][info ][redefine,class,normalize   ] redefined class org.jooq.impl.UpdatableRecordImpl methods error: added method: org.jooq.impl.UpdatableRecordImpl.type(Lorg/jooq/Name;)Ljava/lang/Class; [5]

which originates in https://github.com/openjdk/jdk/blob/1a8c8e07fee33861d348f7b41fea0e3fd5bbc0af/src/hotspot/share/prims/jvmtiRedefineClasses.cpp#L1190

But as to why

static bool can_add_or_delete(Method* m) {
      // Compatibility mode
  return (AllowRedefinitionToAddDeleteMethods &&
          (m->is_private() && (m->is_static() || m->is_final())));
}

would return false here, I'm not sure.

The sample above was for my Record-based test-case, but a similar output is given for the TableImpl-based test:

[1,622s][debug][redefine,class,load] loading name=org.jooq.impl.TableImpl kind=101 (avail_mem=1817844K)
[1,675s][trace][redefine,class,iklass,purge] Class unloading: should_clean_previous_versions = false
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.getRecordConstructor()Ljava/lang/reflect/Constructor; [0] == old: org.jooq.impl.TableImpl.getRecordConstructor()Ljava/lang/reflect/Constructor; [0]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;)V [1] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;)V [1]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;)V [2] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;)V [2]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [3] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [3]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [4] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [4]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [5] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [5]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;)V [6] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;)V [6]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [7] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [7]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [8] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [8]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [9] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [9]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [10] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [10]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;)V [11] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;)V [11]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [12] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [12]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [13] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [13]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;)V [14] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;)V [14]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;)V [15] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;)V [15]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [16] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [16]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [17] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [17]
[2,049s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [18] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [18]
[2,050s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;)V [19] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;)V [19]
[2,050s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;)V [20] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;)V [20]
[2,050s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<clinit>()V [21] == old: org.jooq.impl.TableImpl.<clinit>()V [21]
[2,050s][info ][redefine,class,normalize   ] redefined class org.jooq.impl.TableImpl methods error: added method: org.jooq.impl.TableImpl.type(Lorg/jooq/Name;)Ljava/lang/Class; [22]
[2,056s][debug][redefine,class,load        ] loading name=org.jooq.impl.TableImpl kind=101 (avail_mem=1918716K)
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.getRecordConstructor()Ljava/lang/reflect/Constructor; [0] == old: org.jooq.impl.TableImpl.getRecordConstructor()Ljava/lang/reflect/Constructor; [0]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;)V [1] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;)V [1]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;)V [2] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;)V [2]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [3] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [3]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [4] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [4]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [5] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [5]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;)V [6] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;)V [6]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [7] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;Lorg/jooq/Condition;)V [7]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [8] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [8]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [9] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;Lorg/jooq/TableOptions;)V [9]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [10] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;Lorg/jooq/ForeignKey;Lorg/jooq/InverseForeignKey;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [10]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;)V [11] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;)V [11]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [12] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [12]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [13] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [13]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;)V [14] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;Lorg/jooq/Table;)V [14]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;)V [15] == old: org.jooq.impl.TableImpl.<init>(Ljava/lang/String;Lorg/jooq/Schema;)V [15]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [16] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Lorg/jooq/Comment;)V [16]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [17] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;Ljava/lang/String;)V [17]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [18] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;[Lorg/jooq/Field;)V [18]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;)V [19] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;Lorg/jooq/Table;)V [19]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;)V [20] == old: org.jooq.impl.TableImpl.<init>(Lorg/jooq/Name;Lorg/jooq/Schema;)V [20]
[2,086s][trace][redefine,class,normalize   ] Method matched: new: org.jooq.impl.TableImpl.<clinit>()V [21] == old: org.jooq.impl.TableImpl.<clinit>()V [21]
[2,086s][info ][redefine,class,normalize   ] redefined class org.jooq.impl.TableImpl methods error: added method: org.jooq.impl.TableImpl.type(Lorg/jooq/Name;)Ljava/lang/Class; [22]

@marcelstoer
Copy link
Contributor Author

Digging around with Unified Logging I've got some insight into where the failure occurs. I've added -Xlog:redefine+class*=trace:stdout as JVM-arg to obtain these.

This is fantastic, thanks! I learned something today.

[2,086s][info ][redefine,class,normalize ] redefined class org.jooq.impl.TableImpl methods error: added method: org.jooq.impl.TableImpl.type(Lorg/jooq/Name;)Ljava/lang/Class; [22]

Hhhmm...so it fails for the type() method in both cases. Haven't figured out yet what conclusion we can draw from that.

(pity they log an error on INFO)

@bdalenoord
Copy link
bdalenoord commented Mar 26, 2025

Another hunch I have based on the error is the location of the type(org.jooq.Name)-method: it is defined in the Fields-interface, and an override is made in the FieldsTrait-interface.

My reproduction for the Record-type is only for version 3.20, where FieldsTrait was added as an interface to the Record. In 3.19, FieldsTrait was not a dependency of the Record.

The FieldsTrait provides a default implementation for the type(org.jooq.Name)-method, which may be (???) causing issues... i.e. attempt to redefine whilst a default implementation is already present.

@marcelstoer
Copy link
Contributor Author

Given enough examples a pattern might emerge. So far with have the jOOQ TableImpl and Record plus what I found last May.

classgraph library cannot be updated beyond 4.8.64 (from early 2020)

I should probably add this to my test and run it with your logging JVM-arg.

@sgerke-1L
Copy link
Contributor

Great insights, thanks for finding the type method. Here is a simple reproducer: https://github.com/mockk/mockk/pull/1366/files

The problem seems to occur when a class is mocked that implements an interface with a default implementation.

bdalenoord pushed a commit to TribeAgency/mockk that referenced this issue Mar 27, 2025
By running the tests with `-Xlog:redefine+class*=trace:stdout`, I gained
an insight into which method was actually causing the error. The minimal
testcase provided by sgerke-1L showed that this hunch was actually
correct, the DefaultMethodTest simulates the issue entirely without the
jOOQ dependency.
bdalenoord pushed a commit to TribeAgency/mockk that referenced this issue Mar 27, 2025
By running the tests with `-Xlog:redefine+class*=trace:stdout`, I gained
an insight into which method was actually causing the error. The minimal
testcase provided by sgerke-1L showed that this hunch was actually
correct, the DefaultMethodTest simulates the issue entirely without the
jOOQ dependency, showing that the default method is again the issue.
@bdalenoord
Copy link

@sgerke-1L Indeed, with your minimal testcase the output is very similar:

[2,514s][trace][redefine,class,normalize          ] Method matched: new: io.mockk.core.ClassImplementingInterfaceWithDefaultMethod.<init>()V [0] == old: io.mockk.core.ClassImplementingInterfaceWithDefaultMethod.<init>()V [0]
[2,515s][info ][redefine,class,normalize          ] redefined class io.mockk.core.ClassImplementingInterfaceWithDefaultMethod methods error: added method: io.mockk.core.ClassImplementingInterfaceWithDefaultMethod.foo()I [1]
[2,522s][debug][redefine,class,load               ] loading name=io.mockk.core.ClassImplementingInterfaceWithDefaultMethod kind=101 (avail_mem=7082288K)
[2,524s][trace][redefine,class,normalize          ] Method matched: new: io.mockk.core.ClassImplementingInterfaceWithDefaultMethod.<init>()V [0] == old: io.mockk.core.ClassImplementingInterfaceWithDefaultMethod.<init>()V [0]
[2,524s][info ][redefine,class,normalize          ] redefined class io.mockk.core.ClassImplementingInterfaceWithDefaultMethod methods error: added method: io.mockk.core.ClassImplementingInterfaceWithDefaultMethod.foo()I [1]
> Task :test-modules:client-tests:jvmTest FAILED

and it points to the foo-method here, which is the method with a default implementation.

@sgerke-1L
Copy link
Contributor

@bdalenoord I've updated #1366 to include a fix. I've also asked a question in byte-buddy raphw/byte-buddy#1795 to further clarify if this is a bug in byte-buddy or not.

@sgerke-1L
Copy link
Contributor
sgerke-1L commented Apr 7, 2025

The upstream discussion resulted in the discovery of another potential fix, which is to use decorate instead of redefine. However, I'm not experienced enough with the mockk implementation to judge if that is a valid approach. I've updated my proposed fix in #1366 anyway, maybe someone can sponsor a full test run so we'd learn if the test suite already discovers problems with it.

Let me know if there is anything else I can help with, otherwise I'd see the ball in the maintainer's side of the field now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants
0