easy-random icon indicating copy to clipboard operation
easy-random copied to clipboard

Generic field type issue (composition)

Open seregamorph opened this issue 4 years ago • 7 comments

In addition to #440 there is one more issue related to generic type support. Consider the test:

import static org.assertj.core.api.Assertions.assertThat;

import java.io.Serializable;
import org.junit.jupiter.api.Test;

public class Generic2Test {

    @Test
    void genericComposedShouldBeCorrectlyPopulated() {
        // given
        EasyRandom easyRandom = new EasyRandom();

        // when
        CompositeResource composite = easyRandom.nextObject(CompositeResource.class);

        // then
        assertThat(composite.longResource.getId())
                .isInstanceOf(Long.class);
    }

    static abstract class IdResource<K extends Serializable, T extends IdResource<K, ?>> {

        private K id;

        @SuppressWarnings("unchecked")
        public T setId(K id) {
            this.id = id;
            return (T) this;
        }

        public K getId() {
            return id;
        }
    }

    static class LongResource extends IdResource<Long, LongResource> {
    }

    static class CompositeResource {
        private LongResource longResource;
    }
}

Instead of generating correct beans it fails with NPE:

org.jeasy.random.ObjectCreationException: Unable to create a random instance of type class org.jeasy.random.Generic2Test$CompositeResource

	at org.jeasy.random.EasyRandom.doPopulateBean(EasyRandom.java:172)
	at org.jeasy.random.EasyRandom.nextObject(EasyRandom.java:100)
	at org.jeasy.random.Generic2Test.genericInheritedShouldBeCorrectlyPopulated(Generic2Test.java:16)
	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.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
	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:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	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.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
	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: org.jeasy.random.ObjectCreationException: Unable to create type: org.jeasy.random.Generic2Test$LongResource for field: longResource of class: org.jeasy.random.Generic2Test$CompositeResource
	at org.jeasy.random.FieldPopulator.populateField(FieldPopulator.java:98)
	at org.jeasy.random.EasyRandom.populateField(EasyRandom.java:209)
	at org.jeasy.random.EasyRandom.populateFields(EasyRandom.java:198)
	at org.jeasy.random.EasyRandom.doPopulateBean(EasyRandom.java:165)
	... 67 more
Caused by: org.jeasy.random.ObjectCreationException: Unable to create a random instance of type class org.jeasy.random.Generic2Test$LongResource
	at org.jeasy.random.EasyRandom.doPopulateBean(EasyRandom.java:172)
	at org.jeasy.random.FieldPopulator.generateRandomValue(FieldPopulator.java:160)
	at org.jeasy.random.FieldPopulator.populateField(FieldPopulator.java:93)
	... 70 more
Caused by: java.lang.NullPointerException
	at org.jeasy.random.FieldPopulator.getParametrizedType(FieldPopulator.java:170)
	at org.jeasy.random.FieldPopulator.getRandomizer(FieldPopulator.java:123)
	at org.jeasy.random.FieldPopulator.populateField(FieldPopulator.java:79)
	at org.jeasy.random.EasyRandom.populateField(EasyRandom.java:209)
	at org.jeasy.random.EasyRandom.populateFields(EasyRandom.java:198)
	at org.jeasy.random.EasyRandom.doPopulateBean(EasyRandom.java:165)
	... 72 more

seregamorph avatar Nov 09 '20 08:11 seregamorph

Every new feature comes with new bugs 😄 ER should clearly not fail with a NPE. Thank you for reporting this.

fmbenhassine avatar Nov 14 '20 10:11 fmbenhassine

I have spottet the same and added a simple test here: #450 But did not found a solution yet

reitzmichnicht avatar Dec 16 '20 10:12 reitzmichnicht

@reitzmichnicht you can find a raw solutions here: https://github.com/j-easy/easy-random/compare/master...seregamorph:generic-base-class-classmate - uses com.fasterxml.classmate (jackson also has this dependency) to resolve generic type Alternative impl: https://github.com/j-easy/easy-random/compare/master...seregamorph:generic-base-class-spring-core-no-spring - it is based on the spring-core code, the spring code copied and cleaned up (all unused lines), alternatively spring-core dependency can be used.

These branches were rejected in this project, but you still can find a solution here.

seregamorph avatar Dec 16 '20 11:12 seregamorph

@reitzmichnicht I'm adding your failing test here since #450 was closed.

@Test
void testGenericFieldRandomization() {
    // given
    class Base<T> {
        T t;
    }
    class Concrete {
        Base<String> f;
    }

    // when
    Concrete concrete = easyRandom.nextObject(Concrete.class);

    // then
    assertThat(concrete.f).isInstanceOf(Base.class);
}

This fails with the same stacktrace as above.

fmbenhassine avatar Jan 13 '21 22:01 fmbenhassine

@seregamorph

These branches were rejected in this project, but you still can find a solution here.

Thank you for pointing out these solutions. I just wanted to give a bit of context here about the reasons of rejecting those options.

master...seregamorph:generic-base-class-classmate - uses com.fasterxml.classmate (jackson also has this dependency) to resolve generic type

My concern with those changes is that they are based on #426, which was rejected for the reasons explained in details here: https://github.com/j-easy/easy-random/pull/426#issuecomment-722041902. While your changes are different from those in #426, the new GenericType is what concerning me the most, because java-classmate already defines its own GenericType. That said, I'm not against adding java-classmate as a dependency if it makes our lives easier regarding generic types introspection, and I'm open for contributions to fix this issue in a way that is not based on #426 and which remains relatively easy in order to control complexity.

master...seregamorph:generic-base-class-spring-core-no-spring - it is based on the spring-core code, the spring code copied and cleaned up (all unused lines), alternatively spring-core dependency can be used.

The reasons why this has been rejected were detailed here: https://github.com/j-easy/easy-random/issues/425#issuecomment-723494900.

fmbenhassine avatar Jan 13 '21 23:01 fmbenhassine

I submitted a fix for this issue: https://github.com/j-easy/easy-random/pull/466.

There are a number of challenges around this issue (e.g. issue #440) that my fix does not address and the project is in maintenance mode. Thus, @beans can I ask you to look at it ahead and give me a shout if it has a chance for being accepted and if so, when it can be released?

mjureczko avatar Sep 06 '21 12:09 mjureczko

Did hit the same issue today while upgrading a project. Afaiu the problem is that the RandomizationContext is constructed with the CompositeResource. And from there all the parameterized information is taken. But this is completely wrong if a field/member has nothing to do with the parameterization hierarchy of that class but has it's own one like with LongResource.

The ParameterizedType of LongResource is defined as IdResource<Long, LongResource> and is completely independent of whatever generic information is in CompositeResource or if it doesn't have any at all, isn't?

I locally have a working hack (needs further polishing) where I create a fresh independent 'sub-RandomizationContext' in case I hit a field which has nothing to do with the generic hierarchy of the current class. Not quite sure though if this is the right way to approach it.

struberg avatar Mar 05 '24 13:03 struberg