Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] record interface can't be cast to object #940

Closed
xenoterracide opened this issue Mar 22, 2024 · 18 comments
Closed

[Bug] record interface can't be cast to object #940

xenoterracide opened this issue Mar 22, 2024 · 18 comments

Comments

@xenoterracide
Copy link

Describe the bug

So, I'm playing around with the events being records... I'm not certain what the bug here is, feels like the error message is wrong or could be better. Using prefabs worked, but I had to use them on DomainEvent which is not the thing that the error was complaining about

Steps to reproduce

No response

Error message and version number

java.lang.AssertionError: EqualsVerifier found a problem in class com.xenoterracide.jpa.FooAggregate.
-> argument type mismatch

For more information, go to: https://www.jqno.nl/equalsverifier/errormessages
(EqualsVerifier 3.16, JDK 21.0.2 on Linux)

	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:351)
	at com.xenoterracide.jpa.AbstractSplatTest.abstractAggregateEquality(AbstractSplatTest.java:29)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.IllegalArgumentException: argument type mismatch
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:65)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
	at nl.jqno.equalsverifier.internal.reflection.RecordObjectAccessor.lambda$callRecordConstructor$4(RecordObjectAccessor.java:146)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:34)
	at nl.jqno.equalsverifier.internal.reflection.RecordObjectAccessor.callRecordConstructor(RecordObjectAccessor.java:145)
	at nl.jqno.equalsverifier.internal.reflection.RecordObjectAccessor.makeAccessor(RecordObjectAccessor.java:124)
	at nl.jqno.equalsverifier.internal.reflection.RecordObjectAccessor.scramble(RecordObjectAccessor.java:66)
	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:179)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.realizeCacheFor(PrefabValues.java:162)
	at nl.jqno.equalsverifier.internal.prefabvalues.factories.AbstractGenericFactory.determineAndCacheActualTypeTag(AbstractGenericFactory.java:56)
	at nl.jqno.equalsverifier.internal.prefabvalues.factories.AbstractGenericFactory.determineAndCacheActualTypeTag(AbstractGenericFactory.java:42)
	at nl.jqno.equalsverifier.internal.prefabvalues.factories.SimpleGenericFactory.createValues(SimpleGenericFactory.java:36)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.createTuple(PrefabValues.java:175)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.realizeCacheFor(PrefabValues.java:162)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveTuple(PrefabValues.java:93)
	at nl.jqno.equalsverifier.internal.prefabvalues.PrefabValues.giveOther(PrefabValues.java:130)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$changeField$2(FieldModifier.java:119)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.wrappedChange(FieldModifier.java:140)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.lambda$change$3(FieldModifier.java:135)
	at nl.jqno.equalsverifier.internal.util.Rethrow.lambda$rethrow$1(Rethrow.java:51)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:34)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:49)
	at nl.jqno.equalsverifier.internal.util.Rethrow.rethrow(Rethrow.java:59)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.change(FieldModifier.java:135)
	at nl.jqno.equalsverifier.internal.reflection.FieldModifier.changeField(FieldModifier.java:122)
	at nl.jqno.equalsverifier.internal.reflection.InPlaceObjectAccessor.scrambleInternal(InPlaceObjectAccessor.java:80)
	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.getRedAccessor(ClassAccessor.java:210)
	at nl.jqno.equalsverifier.internal.reflection.ClassAccessor.getRedObject(ClassAccessor.java:186)
	at nl.jqno.equalsverifier.internal.util.Configuration.ensureUnequalExamples(Configuration.java:219)
	at nl.jqno.equalsverifier.internal.util.Configuration.build(Configuration.java:114)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.buildConfig(SingleTypeEqualsVerifierApi.java:424)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.performVerification(SingleTypeEqualsVerifierApi.java:410)
	at nl.jqno.equalsverifier.api.SingleTypeEqualsVerifierApi.verify(SingleTypeEqualsVerifierApi.java:347)
	... 4 more
Caused by: java.lang.ClassCastException: Cannot cast java.lang.Object to com.xenoterracide.jpa.EntityIdentifiable

Code: EqualsVerifier invocation

    EqualsVerifier.forClass(FooAggregate.class)
      .withRedefinedSuperclass()
      .withPrefabValues(AbstractIdentity.class, FooAggregate.Id.create(), BarEntity.Id.create())
      .withPrefabValues(BarEntity.Id.class, BarEntity.Id.create(), BarEntity.Id.create())
      .withPrefabValues(FooAggregate.Id.class, FooAggregate.Id.create(), FooAggregate.Id.create())
      .withPrefabValues(BarEntity.class, BarEntity.create(null, "bar"), BarEntity.create(null, "baz"))
      .suppress(Warning.SURROGATE_OR_BUSINESS_KEY)
      .suppress(Warning.TRANSIENT_FIELDS)
      .withOnlyTheseFields("id", "version", "dirty")
      .verify();

Code: class under test

// © Copyright 2024 Caleb Cushing
// SPDX-License-Identifier: AGPL-3.0-or-later

package com.xenoterracide.jpa;

import static com.xenoterracide.tools.java.function.PredicateTools.prop;
import static java.util.function.Predicate.isEqual;

import com.github.f4b6a3.uuid.UuidCreator;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Transient;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.hibernate.envers.AuditMappedBy;
import org.hibernate.envers.Audited;
import org.jspecify.annotations.NonNull;

@Entity
@Audited
public class FooAggregate extends AbstractAggregate<FooAggregate.@NonNull Id, @NonNull FooAggregate> {

  private String name;

  private Set<@NonNull BarEntity> bars = new HashSet<>();

  protected FooAggregate() {}

  public FooAggregate(@NonNull Id id, @NonNull String name) {
    super(id);
    this.name = name;
  }

  static @NonNull FooAggregate create(@NonNull String name) {
    return new FooAggregate(Id.create(), name);
  }

  protected void registerEvent(
    @NonNull EntityIdentifiable<@NonNull UUID, BarEntity.@NonNull Id, @NonNull BarEntity> event
  ) {
    super.registerEvent(FooEvent.create(this.getId(), event));
  }

  public @NonNull BarEntity addBar(@NonNull String name) {
    var bar = BarEntity.create(this, name);
    this.bars.add(bar);
    return bar;
  }

  public void changeBarName(BarEntity.@NonNull Id id, @NonNull String name) {
    this.bars.stream().filter(prop(b -> b.getId(), isEqual(id))).findAny().ifPresent(e -> e.changeName(name));
  }

  @AuditMappedBy(mappedBy = "foo")
  @OneToMany(orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "foo")
  @NonNull
  Set<@NonNull BarEntity> getBars() {
    return this.bars;
  }

  void setBars(@NonNull Set<@NonNull BarEntity> bars) {
    this.bars = bars;
  }

  @NotNull
  @Column(nullable = false)
  public @NonNull String getName() {
    return name;
  }

  void setName(@NonNull String name) {
    this.name = name;
  }

  @Override
  protected boolean canEqual(@NonNull AbstractEntity<?> that) {
    return that instanceof FooAggregate;
  }

  public static class Id extends AbstractIdentity<@NonNull UUID> {

    @Serial
    @Transient
    private static final long serialVersionUID = 1L;

    protected Id() {}

    private Id(@NonNull UUID id) {
      super(id);
    }

    public static Id create() {
      return new Id(UuidCreator.getTimeOrderedEpoch());
    }

    @Override
    protected boolean canEqual(@NonNull AbstractIdentity<?> that) {
      return that instanceof Id;
    }
  }
}
// © Copyright 2024 Caleb Cushing
// SPDX-License-Identifier: AGPL-3.0-or-later

package com.xenoterracide.jpa;

import jakarta.persistence.MappedSuperclass;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.NonNull;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.AfterDomainEventPublication;
import org.springframework.data.domain.DomainEvents;

/**
 * An abstract class for Domain Aggregates.
 *
 * @param <ID> type parameter
 * @param <EVENT> type parameter
 */
@MappedSuperclass
public abstract class AbstractAggregate<
  ID extends AbstractIdentity<? extends Serializable>, SELF extends AbstractAggregate<ID, ?>
>
  extends AbstractEntity<ID> {

  private final @Transient List<DomainEvent<?, ID, SELF, ?>> domainEvents = new ArrayList<>();

  /**
   * NO-OP parent constuctor for JPA only.
   */
  protected AbstractAggregate() {}

  /**
   * Instantiates a new Abstract aggregate.
   *
   * @param id the id
   */
  protected AbstractAggregate(@NonNull ID id) {
    super(id);
  }

  /**
   * Registers domain events.
   *
   * @param <E> the event type
   * @param event the event
   */
  protected void registerEvent(@NonNull DomainEvent<?, ID, SELF, ?> event) {
    this.domainEvents.add(event);
    this.markDirty();
  }

  /**
   * Clears domain events.
   */
  @AfterDomainEventPublication
  protected void clearDomainEvents() {
    this.domainEvents.clear();
  }

  /**
   * Domain events collection.
   *
   * @return the collection
   */
  @DomainEvents
  protected @NonNull Collection<DomainEvent<?, ID, SELF, ?>> domainEvents() {
    return Collections.unmodifiableList(this.domainEvents);
  }
}
// © Copyright 2024 Caleb Cushing
// SPDX-License-Identifier: AGPL-3.0-or-later

package com.xenoterracide.jpa;

import java.io.Serializable;
import java.time.ZonedDateTime;
import org.jspecify.annotations.NonNull;

/**
 * A domain event.
 *
 * @param <EVENTID> the type parameter
 * @param <AID> the type parameter
 * @param <AGG> the type parameter
 * @param <PAYLOAD> the type parameter
 * @param id  the id
 * @param occurredOn the time the event occurred
 * @param payload the event payload
 * @param aggregate the aggregate the event is associated with
 */
public record DomainEvent<
  EVENTID extends Serializable,
  AID extends AbstractIdentity<?>,
  AGG extends AbstractAggregate<AID, ?>,
  PAYLOAD extends EntityIdentifiable<?, ?, ?>
>(
  @NonNull EVENTID id,
  @NonNull ZonedDateTime occurredOn,
  @NonNull EntityIdentifiable<?, AID, AGG> aggregate,
  @NonNull PAYLOAD payload
) {}

Additional context

looks like adding a prefab works

      .withPrefabValues(
        DomainEvent.class,
        FooEvent.create(FooAggregate.Id.create(), new BarEntity.NameChanged(BarEntity.Id.create(), "foo")),
        FooEvent.create(FooAggregate.Id.create(), new BarEntity.NameChanged(BarEntity.Id.create(), "bar"))
      )
@jqno
Copy link
Owner

jqno commented Mar 23, 2024

Maybe it's because the record has generic parameters, I'll try that out and see if I can come up with a better error message.

@jqno
Copy link
Owner

jqno commented Mar 25, 2024

My initial thought was incorrect; In the example below, EqualsVerifier can handle the generics just fine:

import java.util.Objects;
import org.junit.jupiter.api.Test;

public class ScratchTest {

    @Test
    void equalsverifier() {
        EqualsVerifier.forClass(Aggregate.class).verify();
    }

    public static final class Aggregate<E extends EntityIdentifiable> {

        private final DomainEvent<E> event;

        public Aggregate(DomainEvent<E> event) {
            this.event = event;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Aggregate)) {
                return false;
            }
            Aggregate<E> other = (Aggregate<E>) obj;
            return Objects.equals(event, other.event);
        }

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

    public abstract static class EntityIdentifiable {}

    public static class ConcreteEntityIdentifiable extends EntityIdentifiable {}

    public static record DomainEvent<E extends EntityIdentifiable>(E e) {}
}

So I tried to copy the code you provided into my editor, and unfortunately it lit up like a christmas tree with errors. I was able to resolve some (by adding a Jakarta dependency, redirecting the @NotNull to a different package, removing some annotations, etc) but after about 30 minutes of tinkering I found that a class named AbstractIdentity was missing, which seems pretty important and doesn't seem to come from any of the imports, so I suppose it's one of yours? I think it also contains the equals method that you're testing. But there's also a lot of stuff that is there that isn't relevant, like a bunch of methods that don't do anything for equals but do pull in dependencies that I don't have (for example, the changeBarName method that depends on something called prop).

So please, if you want me to look into this, give me some code that's both minimal and complete.

@xenoterracide
Copy link
Author

xenoterracide commented Mar 25, 2024

I'll try to get back to this in a day or two. That class is here (domainEvents aren't).

#938 (comment)

Sorry, I keep hoping the cause of these things is more obvious at a glance. I expect people to be me, and am surprised when they aren't, sorry. I was hoping your assumption was right.

An educated guess... adding this code to the abstract aggregate linked in the issue above.

  private final @Transient List<DomainEvent<?, ID, SELF, ?>> domainEvents = new ArrayList<>();

  protected void registerEvent( DomainEvent<?, ID, SELF, ?> event) {
    this.domainEvents.add(event);
    this.markDirty();
  }

  protected void clearDomainEvents() {
    this.domainEvents.clear();
  }

  protected @NonNull Collection<DomainEvent<?, ID, SELF, ?>> domainEvents() {
    return Collections.unmodifiableList(this.domainEvents);
  }

otherwise, yeah, I'll give it a stab tomorrow or something.

@jqno
Copy link
Owner

jqno commented Mar 26, 2024

Yeah, sometimes I'm able to see what the problem is by just looking at the code provided, but even then, it's helpful for me if there's as little code as possible, so I don't get distracted by irrelevant things. But in this case, the issue isn't obvious, unfortunately.

I'm not sure what your guess is guessing at?

Anyway, let me know if you managed to reduce this code, and I'll be happy to take a look.

@xenoterracide
Copy link
Author

ok, cool, uh, I finally got back to a point where I can actually try to reduce this, as it's the last error before merge.

@xenoterracide
Copy link
Author

I have no idea what a minimal reproduction of this looks like... I added code, and added more, and it still passed, kind of looks like I have to repro the full hierarchy to do this... I'm certain there's stuff in there that's not needed, but I can't begin to guess at what.

@xenoterracide
Copy link
Author

I'm sure this can be reduced farther but... I'm not entirely certain how.

package com.xenoterracide.jpa;

import java.io.Serial;
import java.io.Serializable;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import nl.jqno.equalsverifier.EqualsVerifier;
import nl.jqno.equalsverifier.Warning;
import org.junit.jupiter.api.Test;

public class EvTest {

  @Test
  void tev() {
    EqualsVerifier.forClass(FooAggregate.class)
      .withRedefinedSuperclass()
      .withPrefabValues(BarEntity.Id.class, BarEntity.Id.create(), BarEntity.Id.create())
      .withPrefabValues(FooAggregate.Id.class, FooAggregate.Id.create(), FooAggregate.Id.create())
      .withPrefabValues(BarEntity.class, BarEntity.create(null, "bar"), BarEntity.create(null, "baz"))
      .suppress(Warning.SURROGATE_OR_BUSINESS_KEY)
      .withOnlyTheseFields("id")
      .verify();
  }

  public interface EntityId<ID extends Serializable, E> {
    ID id();
    Class<E> type();
  }

  public record DEvent<
    EVENTID extends Serializable,
    AID extends AbstractIdentity<?>,
    AGG extends AbstractEntity<AID>,
    PAYLOAD extends EntityId<?, ?>
  >(EVENTID id, ZonedDateTime occurredOn, EntityId<AID, AGG> aggregate, PAYLOAD payload) {}

  public abstract static class AbstractEntity<ID extends AbstractIdentity<? extends Serializable>> {

    private final List<DEvent<?, ID, ?, ?>> domainEvents = new ArrayList<>();
    private ID id;

    protected AbstractEntity() {}

    protected AbstractEntity(ID id) {
      this.id = id;
    }

    public ID getId() {
      return this.id;
    }

    void setId(ID id) {
      this.id = id;
    }

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

    protected abstract boolean canEqual(AbstractEntity<?> that);

    @Override
    public final boolean equals(Object other) {
      if (other instanceof AbstractEntity<?> that) {
        // CHECKSTYLE.OFF: UnnecessaryParentheses
        return (that.canEqual(this) && Objects.equals(this.id, that.id));
        // CHECKSTYLE.ON: UnnecessaryParentheses
      }
      return false;
    }
  }

  public abstract static class AbstractIdentity<ID extends Serializable> implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    private ID id;

    protected AbstractIdentity() {}

    protected AbstractIdentity(ID id) {
      this.id = Objects.requireNonNull(id);
    }

    @Override
    public final int hashCode() {
      return Objects.hashCode(this.id);
    }

    protected abstract boolean canEqual(AbstractIdentity<?> that);

    @Override
    public final boolean equals(Object other) {
      if (other instanceof AbstractIdentity<?> that) {
        return that.canEqual(this) && Objects.equals(this.id, that.id);
      }
      return false;
    }

    @Override
    public String toString() {
      return String.valueOf(this.id);
    }
  }

  public static class BarEntity extends AbstractEntity<BarEntity.Id> {

    private String name;

    private FooAggregate foo;

    BarEntity(BarEntity.Id id, FooAggregate foo, String name) {
      super(id);
      this.foo = foo;
      this.name = name;
    }

    public BarEntity() {}

    static BarEntity create(FooAggregate foo, String name) {
      return new BarEntity(BarEntity.Id.create(), foo, name);
    }

    public FooAggregate getFoo() {
      return foo;
    }

    void setFoo(FooAggregate foo) {
      this.foo = foo;
    }

    @Override
    protected boolean canEqual(AbstractEntity<?> that) {
      return that instanceof BarEntity;
    }

    public String getName() {
      return name;
    }

    void setName(String name) {
      this.name = name;
    }

    public static class Id extends AbstractIdentity<UUID> {

      @Serial
      private static final long serialVersionUID = 1L;

      protected Id() {}

      private Id(UUID id) {
        super(id);
      }

      public static Id create() {
        return new Id(UUID.randomUUID());
      }

      @Override
      protected boolean canEqual(AbstractIdentity<?> that) {
        return that instanceof Id;
      }
    }
  }

  public static class FooAggregate extends AbstractEntity<FooAggregate.Id> {

    private String name;

    private Set<BarEntity> bars = new HashSet<>();

    protected FooAggregate() {}

    public FooAggregate(FooAggregate.Id id, String name) {
      super(id);
      this.name = name;
    }

    static FooAggregate create(String name) {
      return new FooAggregate(FooAggregate.Id.create(), name);
    }

    public BarEntity addBar(String name) {
      var bar = BarEntity.create(this, name);
      this.bars.add(bar);
      return bar;
    }

    Set<BarEntity> getBars() {
      return this.bars;
    }

    void setBars(Set<BarEntity> bars) {
      this.bars = bars;
    }

    public String getName() {
      return name;
    }

    void setName(String name) {
      this.name = name;
    }

    @Override
    protected boolean canEqual(AbstractEntity<?> that) {
      return that instanceof FooAggregate;
    }

    public static class Id extends AbstractIdentity<UUID> {

      @Serial
      private static final long serialVersionUID = 1L;

      protected Id() {}

      private Id(UUID id) {
        super(id);
      }

      public static Id create() {
        return new Id(UUID.randomUUID());
      }

      @Override
      protected boolean canEqual(AbstractIdentity<?> that) {
        return that instanceof Id;
      }
    }
  }
}

@jqno
Copy link
Owner

jqno commented Mar 28, 2024

I'm sure this can be reduced farther but... I'm not entirely certain how.

It doesn't need to be perfect. The point is that I was able to copy/paste this into my editor and reproduce the issue within seconds, so thanks for that. I'll take a closer look in the coming days.

@jqno
Copy link
Owner

jqno commented Mar 28, 2024

I think as a workaround you should be able to add prefab values for DEvent

@jqno
Copy link
Owner

jqno commented Mar 28, 2024

This is the smallest I can get it:

package com.xenoterracide.jpa;

import java.io.Serializable;
import java.util.Objects;
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;

public class EvTest {

    @Test
    void tev() {
        EqualsVerifier.forClass(FooAggregate.class).verify();
    }

    public record DEvent<EVENTID extends Serializable>(EVENTID id) {}

    public static class FooAggregate {

        private DEvent<?> domainEvents;
        private String id;

        @Override
        public final boolean equals(Object other) {
            if (other instanceof FooAggregate that) {
                return Objects.equals(this.id, that.id);
            }
            return false;
        }
    }
}

If you want to know how I did it; I added a little piece to the website the other day: https://jqno.nl/equalsverifier/submit-an-issue/ (I'm also linking to it from the issue template now)

Haven't had time to look into why it fails yet, or why my earlier hunch was incorrect, because it looks suspiciously similar. To be continued.

@xenoterracide
Copy link
Author

xenoterracide commented Mar 28, 2024

Huh, Yeah I feel like I had something similar to that at some point too... And it wasn't causing the problem... I honestly did try to cut it down from the big thing, It just didn't trigger the issue...

@xenoterracide
Copy link
Author

xenoterracide commented Mar 29, 2024

If you want to know how I did it; I added a little piece to the website the other day: https://jqno.nl/equalsverifier/submit-an-issue/ (I'm also linking to it from the issue template now)

TLDR; sorry, I'm old cynical and salty.

firstly, sorry for being such a bad bug reporter. I've gotten rather used to being abused as a bug reporter in the java community; or having to abuse to get someone to take action. I've been dealing with things that are far more painful and critical than this project. Gradle has been a daily war. One thing that has ALWAYS (for ~20 years) bugged me, though. An open source project developer assuming we aren't working on our own projects with our own time. That's usually a complaint I've had with "patches welcome" vs what you're asking for though. I've even contributed small patches to the java world, only to have my code "stolen" (that's what I call taking my code doing a slight modification and committing it without retaining my authorship). What you're asking for is reasonable in request, but (and not your fault) less reasonable in practice due to hurdles created by the Java world. Basically, creating a minimal example is often a lot of work.

I've had a thought on how to remediate this given our current tools. 1, github provides repository templates. 80% of the pain of a minimal example is setting up the build. some projects (checkstyle) expect your minimal example to be compiled in /var/tmp (no seriously, read their issue template and ask, is this not nutty?). Unfortunately we can't all have what prettier has, a server where we can paste code and have the sample produce. Too bad that doesn't have a github way to be integrated. So, solution part 1, use githubs repository template so people can simply clone and own. This repo needs to contain a build structure, and at least 1 modifiable source file that's as complete as can be to doing the thing. In your case? probably a test that passes equalsverifier.

Next step, your issue template needs to provide full instructions on how to do it. My current thought is basically an inline copy and paste-able shell script.

git clone ...
./gradlew build # (or whatever)
git archive # create a zip, upload, we profit

This is an idea I've been having, you don't have to, setting this up and maintaining it is it's own amount of work.

again, sorry, I've not been a great reporter, I'm just burned out. I don't want to be reporting problems anymore. I just want to build the thing I'm building. You've been great to work with, hopefully I can give you a break after this issue.

@jqno
Copy link
Owner

jqno commented Apr 2, 2024

Hey, I'm sorry to hear you've had bad experiences in the past. People being rude (or worse) is, unfortunately, a thing that exists in the open source world.

I get that you have other work to get done too, but so do I. We have to meet in the middle somewhere.

I do feel I have to defend Checkstyle a little, though. I looked at their template, and I honestly don't believe you really need to run their steps in /var/tmp. It's just where the person who made the template was preparing it. I see plenty of tickets being closed successfully that weren't run in /var/www. They do expect you to follow their steps pretty closely, and I totally understand that. They have over 800 open issues and if I look at the contributors, only one or two people who are actually doing significant work on the project. It's just a bandwidth problem; they'd get nothing done otherwise.

On a much smaller scale, it's the same with me. I had half an hour for EqualsVerifier last Thursday, and I spent it reducing your code sample. That's fine, but it also means that I couldn't spend that same time actually looking for a fix. Then the Easter weekend came, and this week will be a busy one. I don't know when I'll next have time to look at your ticket. Again, I don't mind, but if you're actually waiting for a fix, here's how you could have accelerated it.

I'll keep you posted about a potential fix.

@xenoterracide
Copy link
Author

I was just giving you some context on what I'm saying. Their template could be better. The wrote it like they expect you to copy/paste it, and it's not viable. I honestly think copy-paste is what we all need (to improve minimal viable), that's just a good example of how not to do it. If I ever get people using my java stuff like I used to (a decade ago) for Perl, I'll probably try to dogfood this idea.

On a much smaller scale, it's the same with me. I had half an hour for EqualsVerifier last Thursday, and I spent it reducing your code sample.

yeah, I suck. If it helps I spent more getting it that far :/ I don't know how you distilled it that much (I applaud you). I know I had tried simpler generic scenarios, but they didn't pan out. Admittedly I kind of threw my hands up in the air once I realized I was getting closer to reproducing the whole thing trying to find it.

fortunately it's not impeding me. Although I think I've disabled the test for now, because IIRC I had trouble with prefabs in a more production code scenario, or something. Error looked the same, and I couldn't be bothered.

@jqno
Copy link
Owner

jqno commented Apr 2, 2024

yeah, I suck

No you don't. What you did here was extremely helpful. If you'd opened with that one, I would have been very happy :)

@jqno
Copy link
Owner

jqno commented Apr 3, 2024

Found it.

I managed to reduce even further (it was a fun puzzle, got me nerd-sniped for a moment 😅)

record DEvent<T extends Serializable>(T t)
record FooAggregate(DEvent<?> e) {}

The problem was that DEvent expects something that's extends Serializable, but EqualsVerifier only looked at the ? wildcard in FooAggregate's field, and inferred Object.

You can workaround by changing the field to DEvent<? extends Serializable>.

Or you can simply update to version 3.16.1, which is now syncing with Maven Central 🙂

@jqno jqno closed this as completed Apr 3, 2024
@xenoterracide
Copy link
Author

🥃 Cheers.

It's generics all the way down 😜

@jqno
Copy link
Owner

jqno commented Apr 3, 2024

Haha, indeed 😄
I hope it fixes your problem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants