jqno / equalsverifier

EqualsVerifier can be used in Java unit tests to verify whether the contract for the equals and hashCode methods is met.
http://www.jqno.nl/equalsverifier
Apache License 2.0
701 stars 75 forks source link

[Bug] Unable to get `EqualsVerifier` to ignore inaccessible `private static final` field #981

Open cmacq2 opened 1 month ago

cmacq2 commented 1 month ago

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

  1. Create a record with some value such as byte[].
  2. Add a private static final object of some inaccessible type such as HexFormat (delimiter field is inaccessible)
  3. Observe the reported errors, even when attempting to steer EqualsVerifier away 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

jqno commented 1 month ago

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.