easy-random
easy-random copied to clipboard
Generic field type issue (composition)
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
Every new feature comes with new bugs 😄 ER should clearly not fail with a NPE. Thank you for reporting this.
I have spottet the same and added a simple test here: #450 But did not found a solution yet
@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.
@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.
@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.
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?
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.