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

Repeatable random object on the same seed #413

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;

import static org.jeasy.random.util.CollectionUtils.randomElementOf;
import static org.jeasy.random.util.ReflectionUtils.*;

/**
Expand Down Expand Up @@ -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));
Copy link
Member

@fmbenhassine fmbenhassine Oct 31, 2020

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);
}

Copy link
Contributor Author

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

if (randomConcreteSubType == null) {
throw new ObjectCreationException("Unable to find a matching concrete subtype of type: " + fieldType);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

import java.lang.reflect.Constructor;

import static org.jeasy.random.util.CollectionUtils.randomElementOf;
import static org.jeasy.random.util.ReflectionUtils.getPublicConcreteSubTypesOf;
import static org.jeasy.random.util.ReflectionUtils.isAbstract;

Expand All @@ -47,7 +46,7 @@ class ObjenesisObjectFactory implements ObjectFactory {
@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)));
Copy link
Member

@fmbenhassine fmbenhassine Oct 31, 2020

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);
            }
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (randomConcreteSubType == null) {
throw new InstantiationError("Unable to find a matching concrete subtype of type: " + type + " in the classpath");
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ class RandomizationContext implements RandomizerContext {

private final Class<?> type;

private final Random random;

private Object rootObject;

RandomizationContext(final Class<?> type, final EasyRandomParameters parameters) {
this.type = type;
populatedBeans = new IdentityHashMap<>();
stack = new Stack<>();
this.parameters = parameters;
this.random = new Random(parameters.getSeed());
}

void addPopulatedBean(final Class<?> type, Object object) {
Expand Down Expand Up @@ -105,7 +108,7 @@ private List<String> toLowerCase(final List<String> strings) {
}

private int nextInt(int startInclusive, int endExclusive) {
Copy link
Member

@fmbenhassine fmbenhassine Oct 31, 2020

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;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return startInclusive + new Random().nextInt(endExclusive - startInclusive);
return startInclusive + random.nextInt(endExclusive - startInclusive);
}

void setRandomizedObject(Object randomizedObject) {
Expand Down Expand Up @@ -148,4 +151,12 @@ public Object getRootObject() {
public EasyRandomParameters getParameters() {
return parameters;
}

@Override
public <T> T randomElementOf(List<T> list) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

if (list.isEmpty()) {
return null;
}
return list.get(nextInt(0, list.size()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package org.jeasy.random.api;

import java.util.List;
import org.jeasy.random.EasyRandom;
import org.jeasy.random.EasyRandomParameters;

Expand Down Expand Up @@ -70,4 +71,12 @@ public interface RandomizerContext {
*/
EasyRandomParameters getParameters();

/**
* Get a random element from the list.
*
* @param list the input list
* @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);
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.jeasy.random.FieldPredicates.named;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -187,6 +188,19 @@ void testRandomizerContext() {
assertThat(a.b.e.name).isEqualTo("bar");
}

@Test
void testRandomElementOf() {
// Given
String[] elements = {"foo", "bar"};

// When
RandomizationContext context = new RandomizationContext(Object.class, parameters);
String element = context.randomElementOf(asList(elements));

// Then
assertThat(element).isIn(elements);
}

static class MyRandomizer implements ContextAwareRandomizer<D> {

private RandomizerContext context;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* The MIT License
*
* Copyright (c) 2020, Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jeasy.random;

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

import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import org.junit.jupiter.api.Test;

public class RepeatableRandomTest {

@Test
public void validateEqualsAndHashCodeSameRandomInstancePoolSize1() {
for (int i = 0; i < 3; i++) {
validateEqualsAndHashCodeSameRandomInstanceImpl(1);
}
}

@Test
public void validateEqualsAndHashCodeSameRandomInstancePoolSize3() {
for (int i = 0; i < 3; i++) {
validateEqualsAndHashCodeSameRandomInstanceImpl(3);
}
}

private void validateEqualsAndHashCodeSameRandomInstanceImpl(int poolSize) {
long seed = ThreadLocalRandom.current().nextLong();

Object instance1 = randomInstance(Pojo.class, seed, poolSize);
// same seed - hence same object expected
Object instance2 = randomInstance(Pojo.class, seed, poolSize);

assertThat(instance1)
.isEqualTo(instance2);
assertThat(instance1.hashCode())
.isEqualTo(instance2.hashCode());
}

private Object randomInstance(Class<?> type, long seed, int poolSize) {
EasyRandom easyRandom = new EasyRandom(new EasyRandomParameters()
.objectPoolSize(poolSize)
.seed(seed)
.stringLengthRange(3, 5)
.collectionSizeRange(3, 4));
return easyRandom.nextObject(type);
}

public static class Pojo {

private String id;

private List<PojoA> a;

private List<PojoB> b;

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Pojo that = (Pojo) o;
return id.equals(that.id)
&& a.equals(that.a)
&& b.equals(that.b);
}

@Override
public int hashCode() {
return Objects.hash(id, a, b);
}

@Override
public String toString() {
return "Pojo{" +
"id='" + id + '\'' +
", a=" + a +
", b=" + b +
'}';
}
}

public static class PojoA {

private String s;

// equals/hashCode/toString by id to avoid possible stack overflow
private Pojo root;

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PojoA that = (PojoA) o;
return s.equals(that.s)
&& root.id.equals(that.root.id);
}

@Override
public int hashCode() {
return Objects.hash(s, root.id);
}

@Override
public String toString() {
return "PojoA{" +
"s='" + s + '\'' +
", root.id=" + root.id +
'}';
}
}

public static class PojoB {

private String s;

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PojoB that = (PojoB) o;
return s.equals(that.s);
}

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

@Override
public String toString() {
return "PojoB{" +
"s='" + s + '\'' +
'}';
}
}
}

This file was deleted.