Skip to content

[Bug] Wrong order of arguments to constructors due to non-deterministic methods #1160

@lycoris106

Description

@lycoris106

Describe the bug

Description

The non-deterministic bug below was found with NonDex, which explores non-determinism in tests. The order of arguments to constructor can be mixed-up due to assumption of fixed-order in getDeclaredFields.

I have identified the root cause below and will soon submit a PR which mentions and fixes this issue.

Root Cause

The bug is at equalsverifier/internal/instantiation/InstanceCreator.java:

private T createRecordInstance(Map<Field, Object> values) {
    var params = new ArrayList<Object>();
    traverseFields(values, (p, v) -> params.add(v)); 
    var recordProbe = new RecordProbe<T>(type);
    return recordProbe.callRecordConstructor(params);
}

private void traverseFields(Map<Field, Object> values, BiConsumer<FieldProbe, Object> setValue) {
    for (FieldProbe p : fields(type)) {
        Object value = values.get(p.getField());
        if (value == null) {
            value = PrimitiveMappers.DEFAULT_VALUE_MAPPER.get(p.getType());
        }
        setValue.accept(p, value);
    }
}

When traverseFields iterates over fields(type), the order is non-deterministic due to getDeclaredFields in:

  private List<FieldProbe> addFieldsFor(Class<?> c) {
        var fields = new ArrayList<FieldProbe>();
        var statics = new ArrayList<FieldProbe>();

        for (Field field : c.getDeclaredFields()) {
...

Therefore, the param collected in createRecordInstance may not follow the order of declaration (e.g. under differnt JVM), then it passes values to wrong fields. For example, in the Failure Reproduction below, the arguments one and null is swapped.

Steps to reproduce

  • Java version:
openjdk 21.0.9 2025-10-21
OpenJDK Runtime Environment (build 21.0.9+10-Ubuntu-124.04)
OpenJDK 64-Bit Server VM (build 21.0.9+10-Ubuntu-124.04, mixed mode, sharing)
mvn edu.illinois:nondex-maven-plugin:2.2.1:nondex -pl core \
    -Djacoco.skip -Drat.skip -Dpmd.skip -Denforcer.skip \
    -Dtest=com.ericsson.bss.cassandra.ecchronos.core.TestLockCache#testEqualsContract
  • Since I already identified the root cause, I did not try to write a simpler reproduction test

Error message and version number

[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.133 s <<< FAILURE! -- in com.ericsson.bss.cassandra.ecchronos.core.TestLockCache
[ERROR] com.ericsson.bss.cassandra.ecchronos.core.TestLockCache.testEqualsContract -- Time elapsed: 0.525 s <<< FAILURE!
java.lang.AssertionError: 
EqualsVerifier found a problem in class com.ericsson.bss.cassandra.ecchronos.core.LockCache$LockKey.
-> Record: failed to run constructor for record type com.ericsson.bss.cassandra.ecchronos.core.LockCache$LockKey
   These were the values passed to the constructor: [one, null]
   If the record does not accept null values for its constructor parameters, consider suppressing Warning.NULL_FIELDS.

Code: EqualsVerifier invocation

...
    @Test
    public void testEqualsContract()
    {
        EqualsVerifier.forClass(LockCache.LockKey.class).usingGetClass().verify();
    }

Code: class under test

Code under test:

public final class LockCache
{
    private static final Logger LOG = LoggerFactory.getLogger(LockCache.class);

    private final Cache<LockKey, LockException> myFailureCache;
    private final LockSupplier myLockSupplier;

    public LockCache(final LockSupplier lockSupplier, final long expireTimeInSeconds)
    {
        this(lockSupplier, expireTimeInSeconds, TimeUnit.SECONDS);
    }
...
    record LockKey(String dataCenter, @NotNull String resource)
    {
        LockKey
        {
            checkNotNull(resource);
        }
    }
}

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions