[Bug] Unable to get `EqualsVerifier` to ignore inaccessible `private static final` field
Describe the bug
It appears equals verifier also attempts to reflect on static final class fields. This causes confusing/unexpected behaviour when those fields are not accessible (Java module system).
Steps to reproduce
- Create a record with some value such as
byte[]. - Add a
private static finalobject of some inaccessible type such asHexFormat(delimiter field is inaccessible) - Observe the reported errors, even when attempting to steer
EqualsVerifieraway from the offending field by asking it to ignore it.
Error message and version number
java.lang.AssertionError: EqualsVerifier found a problem in class <package>.Test$Foo.
-> The class is not accessible via the Java Module system. Consider opening the module that contains it.
For more information, go to: https://www.jqno.nl/equalsverifier/errormessages
(EqualsVerifier 3.16.1, JDK 21.0.2 on Linux)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:349)
at <package>.Test.test(Test.java:66)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: nl.jqno.equalsverifier.internal.exceptions.ModuleException
at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.handleInaccessibleObjectException(InPlaceObjectAccessor.java:97)
at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.scrambleInternal(InPlaceObjectAccessor.java:86)
at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.scramble(InPlaceObjectAccessor.java:58)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedAccessor(ClassAccessor.java:225)
at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:199)
at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.giveInstances(FallbackFactory.java:99)
at nl.jqno.equalsverifier.internal.prefabvalues.factories.FallbackFactory.createValues(FallbackFactory.java:40)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.createTuple(PrefabValues.java:189)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.realizeCacheFor(PrefabValues.java:172)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveTuple(PrefabValues.java:93)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveOther(PrefabValues.java:131)
at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveOther(PrefabValues.java:107)
at nl.jqno.equalsverifier.internal.reflection.RecordObjectAccessor.withChangedField(RecordObjectAccessor.java:108)
at nl.jqno.equalsverifier.internal.checkers.fieldchecks.MutableStateFieldCheck.execute(MutableStateFieldCheck.java:49)
at nl.jqno.equalsverifier.internal.checkers.FieldInspector.check(FieldInspector.java:29)
at nl.jqno.equalsverifier.internal.checkers.FieldsChecker.check(FieldsChecker.java:73)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verifyWithExamples(SingleTypeEqualsVerifierApi.java:466)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:420)
at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:347)
... 69 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.String java.util.HexFormat.delimiter accessible: module java.base does not "opens java.util" to unnamed module @77cd7a0
at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:391)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:367)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:315)
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:183)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:177)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.change(FieldModifier.java:134)
at nl.jqno.equalsverifier.internal.reflection.FieldModifier.changeField(FieldModifier.java:122)
at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.scrambleInternal(InPlaceObjectAccessor.java:80)
... 86 more
Code: EqualsVerifier invocation
EqualsVerifier.forClass(Foo.class).verify();
EqualsVerifier.forClass(Foo.class).withIgnoredFields("HEX").verify();
EqualsVerifier.forClass(Foo.class).withOnlyTheseFields("data").verify();
Code: class under test
public record Foo(byte[] data) {
private static final HexFormat HEX = HexFormat.ofDelimiter(" ");
@Override
public boolean equals(Object obj) {
return obj instanceof Foo && Arrays.equals(data, ((Foo) obj).data);
}
@Override
public int hashCode() {
return Arrays.hashCode(data);
}
@Override
public String toString() {
return "Foo{" + HEX.formatHex(data) + "}";
}
}
Additional context
No response
Sorry for the slow response, I was out of town for a few days.
I'm assuming that you're running your tests on the modulepath, not on the classpath, is that right?
Ignoring fields is intended for whether or not fields should participate in the equals contract. EqualsVerifier still needs to be able to instantiate every field, though, whether it's ignored or not. I realise that this is sometimes unclear, and I'm thinking about ways to make that clearer.
The workaround for your problem is not to ignore the HEX field, but to add prefab values for it:
EqualsVerifier
.forClass(Foo.class)
.withPrefabValues(HexFormat.class, HexFormat.ofDelimiter(" "), HexFormat.ofDelimiter("/"))
.verify();
Please let me know if that helps.
I have the same issue for private final ReentrantLock lock = new ReentrantLock();:
"Field subscriptions of type java.util.List is not accessible via the Java Module System. Consider opening the module that contains it, or add prefab values for type java.util.List."
My fix:
EqualsVerifier.simple()
.forClass(Foo.class)
.withPrefabValues(ReentrantLock.class, new ReentrantLock(), new ReentrantLock())
.withIgnoredFields("lock")
.verify();
This works, but I'd appreciate something less weird.
- For your specific case, I just released EqualsVerifier 3.19.1 with a built-in prefab value for ReentrantLock.
- For @cmacq2 's case, I will add a built-in prefab for HexFormat as well. However, since this class was introduced in Java 17, I'll do that for a future version that has JDK17 as baseline. I'm already working on that.
- ~~For the more general case regarding static final variables, I'm still not sure how I think about it. On the one hand, I want EqualsVerifier to cover all the bases, and static final fields can be used inside of equals methods and cause trouble if not initialized correctly. On the other hand, the chances of static final fields actually causing problems in practice, seem relatively small. Still, I'm hesitant to release this strictness, since EqualsVerifier has always erred on the side of strictness in the past. I'll keep this issue open until I've made a decision.~~
Just out of curiosity - which version of EqualsVerifier are you currenly using? I've made some changes recently and I'm having trouble reproducing the issue. It's probably PEBCAK, but still...
Thanks a lot! I've been using 3.18.2.
There was at least a little bit of PEBCAK going on, because I only now see that your ReentrantLock is private final, but not static. Also, your error mentions java.util.List, not ReentrantLock. That's strange, but I don't think it's related to @cmacq2 's original problem.
For non-static fields of types from other modules, the module error is expected (at least, as long as the type isn't added to the built-in prefab values). If EqualsVerifier doesn't have a prefab value (either built-in or provided by you, the user), it tries to instatiate it by itself, and for that it needs reflection, which means the module should be open to that. Generally, it's preferable to just provide a prefab value and not open the module for reflection.
I've been testing a bit with static final fields, and it turns out EqualsVerifier already excludes them, so my previous remark is incorrect. Please ignore it 😅
@cmacq2 , if you're still seeing this, could you try again with a recent version of EqualsVerifier (for example, 3.19.1) if you're still seeing this problem?
I've just released EqualsVerifier 4.0, which has a built-in prefab value for HexFormat. That should solve the problem, if it wasn't already solved.
If it somehow persists, feel free to open a new issue!