-
Notifications
You must be signed in to change notification settings - Fork 232
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
Repeatable random object on the same seed #413
Conversation
@benas any ideas, objections, or maybe additional clarifications required? |
Hi @seregamorph , thank you for the PR! I did not look at it yet, I have very little spare time during these days which I'm using for another project. I will take a look asap and let you know. Thank you. |
@benas any chance it will be reviewed? |
Good catch! With the same seed, we should end-up with the same generated objects (assuming equals/hashcode are correctly implemented). If this is not the case, I consider it as a bug in Easy Random. I will test your PR in details and get back to you if I need more clarification. Thank you! |
Exactly, this PR addresses an issue with it. |
* | ||
* @author Mahmoud Ben Hassine ([email protected]) | ||
*/ | ||
public final class CollectionUtils { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if this is documented to be intended only for internal use, I do not remove public APIs without notice. Please put this back and mark it as deprecated (See how DateUtils
has been deprecated in the same way).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reverted and marked deprecated
return list.get(nextInt(0, list.size())); | ||
} | ||
|
||
private static int nextInt(int startInclusive, int endExclusive) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually duplicated in RandomizationContext
. Both methods seems to be called always with startInclusive = 0
, which mean this is no different than Random.nextInt(bound)
, so we can get rid of these methods and use the built-in method instead (I will show how in other comments).
* @param <T> the type of elements in the list | ||
* @return a random element from the list or null if the list is empty | ||
*/ | ||
<T> T randomElementOf(List<T> list); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to being a breaking change, this does not belong to the contract of RandomizerContext
. We can manage to have a random element from a list where this is used (ie in FieldPopulator
and ObjenesisObjectFactory
) without this method, see other comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
@@ -128,7 +127,7 @@ private Object generateRandomValue(final Field field, final RandomizationContext | |||
value = mapPopulator.getRandomMap(field, context); | |||
} else { | |||
if (context.getParameters().isScanClasspathForConcreteTypes() && isAbstract(fieldType) && !isEnumType(fieldType) /*enums can be abstract, but can not inherit*/) { | |||
Class<?> randomConcreteSubType = randomElementOf(filterSameParameterizedTypes(getPublicConcreteSubTypesOf(fieldType), fieldGenericType)); | |||
Class<?> randomConcreteSubType = context.randomElementOf(filterSameParameterizedTypes(getPublicConcreteSubTypesOf(fieldType), fieldGenericType)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FieldPopulator has a reference to an EasyRandom instance (which is initialized with the same seed), so this can be replaced with:
List<Class<?>> parameterizedTypes = filterSameParameterizedTypes(getPublicConcreteSubTypesOf(fieldType), fieldGenericType);
if (parameterizedTypes.isEmpty()) {
throw new ObjectCreationException("Unable to find a matching concrete subtype of type: " + fieldType);
} else {
Class<?> randomConcreteSubType = parameterizedTypes.get(easyRandom.nextInt(parameterizedTypes.size()));
value = easyRandom.doPopulateBean(randomConcreteSubType, context);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for exact code sample. done
@@ -47,7 +46,7 @@ | |||
@Override | |||
public <T> T createInstance(Class<T> type, RandomizerContext context) { | |||
if (context.getParameters().isScanClasspathForConcreteTypes() && isAbstract(type)) { | |||
Class<?> randomConcreteSubType = randomElementOf(getPublicConcreteSubTypesOf((type))); | |||
Class<?> randomConcreteSubType = context.randomElementOf(getPublicConcreteSubTypesOf((type))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use a Random
instance here and initialize it (only once) with the seed from the context:
private Random random;
@Override
public <T> T createInstance(Class<T> type, RandomizerContext context) {
if (random == null) {
random = new Random(context.getParameters().getSeed());
}
if (context.getParameters().isScanClasspathForConcreteTypes() && isAbstract(type)) {
List<Class<?>> publicConcreteSubTypes = getPublicConcreteSubTypesOf(type);
if (publicConcreteSubTypes.isEmpty()) {
throw new InstantiationError("Unable to find a matching concrete subtype of type: " + type + " in the classpath");
} else {
Class<?> randomConcreteSubType = publicConcreteSubTypes.get(random.nextInt(publicConcreteSubTypes.size()));
return (T) createNewInstance(randomConcreteSubType);
}
} else {
try {
return createNewInstance(type);
} catch (Error e) {
throw new ObjectCreationException("Unable to create an instance of type: " + type, e);
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -105,7 +108,7 @@ boolean hasExceededRandomizationDepth() { | |||
} | |||
|
|||
private int nextInt(int startInclusive, int endExclusive) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method can be removed and the line where it is called can be replaced with:
int randomIndex = actualPoolSize > 1 ? random.nextInt(actualPoolSize) : 0;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -148,4 +151,12 @@ public Object getRootObject() { | |||
public EasyRandomParameters getParameters() { | |||
return parameters; | |||
} | |||
|
|||
@Override | |||
public <T> T randomElementOf(List<T> list) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be removed as explained below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
Perfect! Thank you for these updates. Rebased, squashed and merged as 15e5a82. Thank you for your contribution 👍 |
The problem:
EasyRandom
object initialized with the sameseed
parameter should generate same POJO instances. This contract came fromjava.util.Random
is respected by most ofRandomizer
implementations. But there is still at least two places that generatenew Random()
object without seed. As a result the generated POJOs are different.The context: in my project I validate equals/hashCode/toString implementations for the POJOs that have back references. I generate random beans by
EasyRandom
instance initialized with sameseed
value and then callequals/hashCode
for such pairs, separate test callstoString()
. Expected result: equals=true, hashCode should also equal, all three methods do not throwStackOverflowError
(if POJO developer is not careful with equals/hashCode/toString exclude (lombok), the call will lead to infinite nested traversals).Possible workarounds: for now it can be solved by setting
EasyRandomParameters().objectPoolSize(1)
. Because if thepopulatedBeans
list collection size is single element, there is no options to randomly choose other than first.Proposed solution: share
Random
object inRandomizationContext
.TDD:
RepeatableRandomTest
has test that should fail with old implementation. It's the minimal model that reproduces the issue.