2

When running equalsverfier in quarkus dev mode, equalsverfier tests fail.

I tried to test a class with equalsverifier. This works in my IDE. I tried to use it in quarkus dev mode (by running ./mvnw quarkus:dev), but then it fails with the following exception:

ERROR [io.qua.test] (Test runner thread) Test DingetjeTest#implementsEquals() failed 
: java.lang.AssertionError: EqualsVerifier found a problem in class a.Dingetje.
-> Can not set final java.lang.String field a.Dingetje.text to a.Dingetje

For more information, go to: http://www.jqno.nl/equalsverifier/errormessages
        at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:308)
        at a.DingetjeTest.implementsEquals(DingetjeTest.java:11)
Caused by: java.lang.IllegalArgumentException: Can not set final java.lang.String field a.Dingetje.text to a.Dingetje
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
        at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.ensureObj(UnsafeFieldAccessorImpl.java:58)
        at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.get(UnsafeQualifiedObjectFieldAccessorImpl.java:38)
        at java.base/java.lang.reflect.Field.get(Field.java:418)
        at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$copyTo$1(FieldModifier.java:79)
        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.copyTo(FieldModifier.java:79)
        at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.copyInto(InPlaceObjectAccessor.java:43)
        at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.copy(InPlaceObjectAccessor.java:24)
        at nl.jqno.equalsverifier.internal.checkers.ExamplesChecker.checkSingle(ExamplesChecker.java:84)
        at nl.jqno.equalsverifier.internal.checkers.ExamplesChecker.check(ExamplesChecker.java:47)
        at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verifyWithExamples(SingleTypeEqualsVerifierApi.java:413)
        at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:369)
        at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:304)
        ... 1 more

Here's the class under test:

package a;

import java.util.Objects;

public class Dingetje {
    private final String text;

    public Dingetje(String text) {
        this.text = text;
    }

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Dingetje)) {
            return false;
        }
        Dingetje other = (Dingetje) o;
        return text.equals(other.text);
    }

    @Override
    public final int hashCode() {
        return Objects.hash(text);
    }
}

And the test:

package a;

import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;

class DingetjeTest {
    @Test
    void implementsEquals() {
        EqualsVerifier.forClass(Dingetje.class)
                .withNonnullFields("text")
                .verify();
    }
}

What am I missing here?

Talip
  • 23
  • 3
  • I'm not familiar with Quarkus, but EqualsVerifier uses a lot of reflection. In Quarkus dev mode, can you do something like `var d = new Dingetje("x"); Dingetje.class.getField("text").set(d, "y");`? (I'm typing this from memory on my phone so adjust where needed.) If not, then that's the culprit. If yes, I guess we need to dig deeper. – jqno Nov 26 '21 at 22:13
  • With getDeclaredField and making the field accessible, the set can be done. So I think we need to dig deeper. This can easily be reproduced by creating a quarkus project with only the 2 above mentioned files. Please let me know if you need me to show the issue. – Talip Nov 29 '21 at 09:20
  • If you can make a self-contained Quarkus project that reproduces the issue on GitHub so that I can clone and run it, and then open an issue on the EqualsVerifier github, I can help you further! – jqno Nov 29 '21 at 09:49
  • I've invited you to the repo, so that you can clone and run it. – Talip Nov 30 '21 at 14:30
  • Thanks! Would you mind opening an issue as well? Then we can continue the conversation there, and post an answer here when the issue is resolved. – jqno Nov 30 '21 at 15:20
  • I was able to reproduce it. The error doesn't occur the first time you run tests, but it does the second time. It has to do with java.lang.Class instances being cached, and the second run happens with a new classloader, so the java.lang.Class instances no longer match. I'm not sure (yet) how to work around that, and I don't know enough about Quarkus to know if there's a workaround on that side. – jqno Dec 01 '21 at 15:06
  • Issue https://github.com/jqno/equalsverifier/issues/550 created – Talip Dec 03 '21 at 15:03
  • Thanks, but I'd already found a fix and released it :). – jqno Dec 03 '21 at 15:32
  • No problem, I’m happy that it is fixed :). Can I already get the 3.8 release from maven central? – Talip Dec 04 '21 at 20:14
  • It took a while to sync, but it looks like it's finally there. – jqno Dec 06 '21 at 06:57

1 Answers1

2

EqualsVerifier uses Objenesis to create instances of classes, and it keeps the same reference of the objenesis object around for performance reasons. It caches all the objects it has created before, so that makes things quicker when you want to create the same object over and over again, which EqualsVerifier tends to do.

However, EqualsVerifier keeps a static reference to objenesis, which means that it lives as long as the JVM does. It turns out that the Quarkus test runner can re-run the same tests again and again, and it creates a new class loader each time. But part of the equality of java.lang.Class is that the classloader that created the class, must also be the same. So it couldn't retrieve these objects from its cache anymore and returnd instances with classloaders that are now different from the other objects created in the test, and this caused the exceptions that you saw.

In version 3.8 of EqualsVerifier (created as a result of this StackOverflow post), this issue can be avoided by adding #withResetCaches() like this:

EqualsVerifier.forClass(Dingetje.class)
    .withResetCaches()
    .withNonnullFields("text")
    .verify();

That fixes the problem.

jqno
  • 15,133
  • 7
  • 57
  • 84