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
708 stars 75 forks source link

ConcurrentModificationException: Unable to verify subclasses of ArrayList collection #341

Closed Alexis2004 closed 4 years ago

Alexis2004 commented 4 years ago

What steps will reproduce the problem?

Verify any class that extends ArrayList collection.

What is the code that triggers this problem?

import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
import org.junit.Test;

import java.util.ArrayList;

public class ArrayListProblemTest {

    public static class TestDTO extends ArrayList<Integer> {
    }

    @Test
    public void testDto() {
        EqualsVerifier
                .forClass(TestDTO.class)
                .suppress(Warning.NULL_FIELDS)
                .verify();
    }

}

What error message or stack trace does EqualsVerifier give?

java.lang.AssertionError: EqualsVerifier found a problem in class ArrayListProblemTest$TestDTO.
-> null

For more information, go to: http://www.jqno.nl/equalsverifier/errormessages

    at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:271)
    at ArrayListProblemTest.testDto(ArrayListProblemTest.java:17)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
    at org.junit.vintage.engine.execution.RunnerExecutor.execute(RunnerExecutor.java:40)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)
    at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
    at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
    at org.junit.vintage.engine.VintageTestEngine.executeAllChildren(VintageTestEngine.java:80)
    at org.junit.vintage.engine.VintageTestEngine.execute(VintageTestEngine.java:71)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)
Caused by: java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList.hashCodeRange(ArrayList.java:621)
    at java.base/java.util.ArrayList.hashCode(ArrayList.java:613)
    at nl.jqno.equalsverifier.internal.util.CachedHashCodeInitializer.getInitializedHashCode(CachedHashCodeInitializer.java:69)
    at nl.jqno.equalsverifier.internal.checkers.ExamplesChecker.checkHashCode(ExamplesChecker.java:134)
    at nl.jqno.equalsverifier.internal.checkers.ExamplesChecker.checkSingle(ExamplesChecker.java:79)
    at nl.jqno.equalsverifier.internal.checkers.ExamplesChecker.check(ExamplesChecker.java:45)
    at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verifyWithExamples(SingleTypeEqualsVerifierApi.java:364)
    at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:321)
    at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:267)
    ... 42 more

What did you expect?

I expect that my DTO can be verified by EqualsVerifier.

Which version of EqualsVerifier are you using?

Latest version 3.4.2

Please provide any additional information below.

It seems that EqualsVerifier library is trying to duplicate object in some strange way that break ArrayList internals integrity.

jqno commented 4 years ago

AbstractList has a very specialized and optimized implementation that is extremely hard to verify directly. The same applies to any of its subclasses. I've tried several times to make it work, but it's just not feasible, unfortunately.

As a workaround you could change your DTO to contain ArrayList instead of overriding it. Comosition over inheritance, right? 😉

I'll see if I can make EqualsVerifier emit a more helpful error message in this situation.

Alexis2004 commented 4 years ago

It's a good idea to use composition instead of inheritance in new code. Unfortunately, I'm trying to introduce unit testing for some legacy DTOs in the project and that DTO is part of the Hibernate entity that have a lot of saved data in production databases. So I will have to implement additional type adapters for each entity to save the current data format, and this doesn't look like an easy task.

So I'm wondering if it is possible to implement special behavior for cloning successors of ArrayList collection? For example, we can try to instantiate a new instance of TestDTO using a parameterless constructor and then adding to it duplicates of all items from source collection using the addAll method. If it's impossible to implement it automatically it would be great to give a manual handle to control the instantiation of problematic classes like this (something similar to withPrefabValues configuration option).

jqno commented 4 years ago

Ah, I see.

Instantiating objects isn't the problem, EqualsVerifier can do that just fine. The issue is that EqualsVerifier will use reflection to change fields and see what happens. This is fine for POJO's, but AbstractList has complicated internal invariants that get screwed up this way. Unfortunately, this is how EqualsVerifier works. There are other ways of course, but those would probably require an (almost) complete re-write of EqualsVerifier. That requires an amount of time that I simply do not have, unfortunately.

jqno commented 4 years ago

Oops, accidentally closed this. I still want to implement a better error message.

jqno commented 4 years ago

I've released version 3.4.3, which contains a more helpful error message.