`IncompatibleClassChangeError` on sealed type fields
(This issue relates to #564 and possibly #638.)
If one applies EqualsVerifier to a class with a field of some sealed type, then an exception is thrown:
...
Caused by: java.lang.IllegalStateException: java.lang.IllegalStateException: Failed to invoke proxy for public abstract java.lang.Class net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles$Lookup.defineClass(java.lang.Object,byte[]) throws java.lang.IllegalAccessException
at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1640)
at net.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:118)
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:6166)
at nl.jqno.equalsverifier.internal.reflection.Instantiator.giveDynamicSubclass(Instantiator.java:98)
at nl.jqno.equalsverifier.internal.reflection.Instantiator.of(Instantiator.java:47)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.buildObjectAccessor(ClassAccessor.java:261)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedAccessor(ClassAccessor.java:179)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:168)
at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.giveInstances(FallbackFactory.java:95)
at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.createValues(FallbackFactory.java:40)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.createTuple(PrefabValues.java:157)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.realizeCacheFor(PrefabValues.java:140)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveTuple(PrefabValues.java:81)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveOther(PrefabValues.java:104)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$changeField$2(FieldModifier.java:97)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.wrappedChange(FieldModifier.java:118)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$change$3(FieldModifier.java:113)
at nl.jqno.equalsverifier.internal.util.Rethrow.lambda$rethrow$0(Rethrow.java:47)
at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:30)
at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:45)
at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:55)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.change(FieldModifier.java:113)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.changeField(FieldModifier.java:100)
at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.scramble(InPlaceObjectAccessor.java:52)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedAccessor(ClassAccessor.java:179)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:168)
at nl.jqno.equalsverifier.internal.util.Configuration.ensureUnequalExamples(Configuration.java:199)
at nl.jqno.equalsverifier.internal.util.Configuration.build(Configuration.java:99)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.buildConfig(SingleTypeEqualsVerifierApi.java:381)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:367)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:312)
... 66 more
Caused by: java.lang.IllegalStateException: Failed to invoke proxy for public abstract java.lang.Class net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup$MethodHandles$Lookup.defineClass(java.lang.Object,byte[]) throws java.lang.IllegalAccessException
at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1173)
at jdk.proxy2/jdk.proxy2.$Proxy60.defineClass(Unknown Source)
at net.bytebuddy.dynamic.loading.ClassInjector$UsingLookup.injectRaw(ClassInjector.java:1638)
... 98 more
Caused by: java.lang.IncompatibleClassChangeError: class com.picnic.testing.Super$$DynamicSubclass$768795248 cannot implement sealed interface com.picnic.testing.TestTest$Super
at java.base/java.lang.ClassLoader.defineClass0(Native Method)
at java.base/java.lang.System$2.defineClass(System.java:2307)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2439)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2416)
at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1843)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at net.bytebuddy.utility.Invoker$Dispatcher.invoke(Unknown Source)
at net.bytebuddy.utility.dispatcher.JavaDispatcher$Dispatcher$ForNonStaticMethod.invoke(JavaDispatcher.java:1028)
at net.bytebuddy.utility.dispatcher.JavaDispatcher$ProxiedInvocationHandler.invoke(JavaDispatcher.java:1158)
... 100 more
Code to reproduce:
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;
public final class SealedFieldTypeTest {
private sealed interface Super {}
private non-sealed interface Sub extends Super {}
static final class Container<T extends Super> {
private final T element;
Container(T element) {
this.element = element;
}
@Override
public boolean equals(Object o) {
return o instanceof Container && ((Container<?>) o).element.equals(element);
}
@Override
public int hashCode() {
return element.hashCode();
}
}
@Test
void equality() {
EqualsVerifier.forClass(Container.class).verify();
}
}
Tested against EqualsVerifier 3.10. The issue can be worked around by adding .withPrefabValues(Super.class, mock(Sub.class), mock(Sub.class)).
Thanks for reporting this, I can reproduce it. It's indeed related to #564. Not sure about #638, since Kotlin (probably) doesn't compile its sealed classes down to Java's sealed classes (yet?), although the solutions to both problems might be similar.
I want to make some smart logic to find final or non-sealed implementations of the sealed class/interface to use as prefab values, but until then, adding prefab values yourself is indeed the way to go. Maybe I could make the error message a bit more helpful in the mean time though.
I think you can leverage the new methods isSealed() and getPermittedSubclasses() added to Class<?> in JDK 17:
assertTrue(Super.class.isSealed());
assertFalse(Sub.class.isSealed());
assertTrue(Sub.class.getSuperclass().isSealed());
assertArrayEquals(Super.class.getPermittedSubclasses(), new Class<?>[]{Sub.class});
Indeed, it shouldn't be too hard...just haven't found time to look into it yet :sweat_smile:
Thanks for reporting this, I can reproduce it. It's indeed related to #564. Not sure about #638, since Kotlin (probably) doesn't compile its sealed classes down to Java's sealed classes (yet?), although the solutions to both problems might be similar.
Kotlin 1.7 combined with Java 17 does compile sealed classes down to Java's sealed classes.
It finally happened! EqualsVerifier 3.14 is now syncing with Maven Central. It fixes this issue and also some other related to sealed types.