Skip to content

Commit

Permalink
Support type variables
Browse files Browse the repository at this point in the history
See [issue](j-easy#425)

Pass the actual type along with a field that will be used to pick
correct randomizer. Consider the following example:
```
abstract class Base<T> {
	T t;
}
class Concrete extends Base<String> {}
```
If we only rely on `Field.getType()`, property `Concrete.t` would be
randomized as `Object` and then cause a runtime exception when used.

Java reflection API provides a way to get the actual type, in this case
`String`. This information is passed in `GenericField` so that the
correct randomizer can be picked later.
  • Loading branch information
Olli Kuonanoja committed Aug 17, 2020
1 parent 1598695 commit c376ec5
Show file tree
Hide file tree
Showing 10 changed files with 337 additions and 28 deletions.
13 changes: 7 additions & 6 deletions easy-random-core/src/main/java/org/jeasy/random/EasyRandom.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import org.jeasy.random.api.*;
import org.jeasy.random.randomizers.misc.EnumRandomizer;
import org.jeasy.random.util.GenericField;
import org.jeasy.random.util.ReflectionUtils;

import java.lang.reflect.Field;
Expand Down Expand Up @@ -148,7 +149,7 @@ <T> T doPopulateBean(final Class<T> type, final RandomizationContext context) {
context.addPopulatedBean(type, result);

// retrieve declared and inherited fields
List<Field> fields = getDeclaredFields(result);
List<GenericField> fields = getDeclaredFields(result);
// we can not use type here, because with classpath scanning enabled the result can be a subtype
fields.addAll(getInheritedFields(result.getClass()));

Expand Down Expand Up @@ -190,17 +191,17 @@ private <T> T randomize(final Class<T> type, final RandomizationContext context)
return null;
}

private <T> void populateFields(final List<Field> fields, final T result, final RandomizationContext context) throws IllegalAccessException {
for (final Field field : fields) {
private <T> void populateFields(final List<GenericField> fields, final T result, final RandomizationContext context) throws IllegalAccessException {
for (final GenericField field : fields) {
populateField(field, result, context);
}
}

private <T> void populateField(final Field field, final T result, final RandomizationContext context) throws IllegalAccessException {
if (exclusionPolicy.shouldBeExcluded(field, context)) {
private <T> void populateField(final GenericField field, final T result, final RandomizationContext context) throws IllegalAccessException {
if (exclusionPolicy.shouldBeExcluded(field.getField(), context)) {
return;
}
if (!parameters.isOverrideDefaultInitialization() && getFieldValue(result, field) != null && !isPrimitiveFieldWithDefaultValue(result, field)) {
if (!parameters.isOverrideDefaultInitialization() && getFieldValue(result, field.getField()) != null && !isPrimitiveFieldWithDefaultValue(result, field.getField())) {
return;
}
fieldPopulator.populateField(result, field, context);
Expand Down
21 changes: 11 additions & 10 deletions easy-random-core/src/main/java/org/jeasy/random/FieldPopulator.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.jeasy.random.api.Randomizer;
import org.jeasy.random.api.RandomizerProvider;
import org.jeasy.random.randomizers.misc.SkipRandomizer;
import org.jeasy.random.util.GenericField;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -68,15 +69,15 @@ class FieldPopulator {
this.mapPopulator = mapPopulator;
}

void populateField(final Object target, final Field field, final RandomizationContext context) throws IllegalAccessException {
void populateField(final Object target, final GenericField field, final RandomizationContext context) throws IllegalAccessException {
Randomizer<?> randomizer = getRandomizer(field, context);
if (randomizer instanceof SkipRandomizer) {
return;
}
if (randomizer instanceof ContextAwareRandomizer) {
((ContextAwareRandomizer<?>) randomizer).setRandomizerContext(context);
}
context.pushStackItem(new RandomizationContextStackItem(target, field));
context.pushStackItem(new RandomizationContextStackItem(target, field.getField()));
if(!context.hasExceededRandomizationDepth()) {
Object value;
if (randomizer != null) {
Expand All @@ -92,10 +93,10 @@ void populateField(final Object target, final Field field, final RandomizationCo
}
}
if (context.getParameters().isBypassSetters()) {
setFieldValue(target, field, value);
setFieldValue(target, field.getField(), value);
} else {
try {
setProperty(target, field, value);
setProperty(target, field.getField(), value);
} catch (InvocationTargetException e) {
String exceptionMessage = String.format("Unable to invoke setter for field %s of class %s",
field.getName(), target.getClass().getName());
Expand All @@ -106,26 +107,26 @@ void populateField(final Object target, final Field field, final RandomizationCo
context.popStackItem();
}

private Randomizer<?> getRandomizer(Field field, RandomizationContext context) {
private Randomizer<?> getRandomizer(GenericField field, RandomizationContext context) {
// issue 241: if there is no custom randomizer by field, then check by type
Randomizer<?> randomizer = randomizerProvider.getRandomizerByField(field, context);
Randomizer<?> randomizer = randomizerProvider.getRandomizerByField(field.getField(), context);
if (randomizer == null) {
randomizer = randomizerProvider.getRandomizerByType(field.getType(), context);
}
return randomizer;
}

private Object generateRandomValue(final Field field, final RandomizationContext context) {
private Object generateRandomValue(final GenericField field, final RandomizationContext context) {
Class<?> fieldType = field.getType();
Type fieldGenericType = field.getGenericType();
Type fieldGenericType = field.getField().getGenericType();

Object value;
if (isArrayType(fieldType)) {
value = arrayPopulator.getRandomArray(fieldType, context);
} else if (isCollectionType(fieldType)) {
value = collectionPopulator.getRandomCollection(field, context);
value = collectionPopulator.getRandomCollection(field.getField(), context);
} else if (isMapType(fieldType)) {
value = mapPopulator.getRandomMap(field, context);
value = mapPopulator.getRandomMap(field.getField(), 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* The MIT License
*
* Copyright (c) 2020, Mahmoud Ben Hassine ([email protected])
*
* 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.util;

import java.lang.reflect.Field;
import java.util.Objects;

/**
* Support type variables for {@link Field}
*
* When using type variables, {@link Field#getType()} will return the lower bound class.
* However, in case the generic field is part of a base class, the actual type is sometimes
* known. This class wraps {@link Field} and contains the actual type as well.
*
*/
public class GenericField {
private final Field field;
private final Class<?> type;

public GenericField(Field field, Class<?> type) {
this.field = field;
this.type = type;
}

public GenericField(Field field) {
this(field, field.getType());
}

/**
* @return The wrapped {@link Field}
*/
public Field getField() {
return field;
}

/**
* @return The type of the field, possibly a resolved type variable
*/
public Class<?> getType() {
return type;
}

public String getName() {
return field.getName();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GenericField that = (GenericField) o;
return Objects.equals(field, that.field) &&
Objects.equals(type, that.type);
}

@Override
public int hashCode() {
return Objects.hash(field, type);
}

@Override
public String toString() {
return "GenericField{" +
"field=" + field +
", type=" + type +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Locale.ENGLISH;
import static java.util.stream.Collectors.toList;

Expand Down Expand Up @@ -102,8 +104,10 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg
* @param <T> the actual type to introspect
* @return list of declared fields
*/
public static <T> List<Field> getDeclaredFields(T type) {
return new ArrayList<>(asList(type.getClass().getDeclaredFields()));
public static <T> List<GenericField> getDeclaredFields(T type) {
return stream(type.getClass().getDeclaredFields())
.map(field -> new GenericField(field, field.getType()))
.collect(Collectors.toCollection(ArrayList::new));
}

/**
Expand All @@ -112,11 +116,24 @@ public static <T> List<Field> getDeclaredFields(T type) {
* @param type the type to introspect
* @return list of inherited fields
*/
public static List<Field> getInheritedFields(Class<?> type) {
List<Field> inheritedFields = new ArrayList<>();
public static List<GenericField> getInheritedFields(Class<?> type) {
List<GenericField> inheritedFields = new ArrayList<>();
while (type.getSuperclass() != null) {
Class<?> superclass = type.getSuperclass();
inheritedFields.addAll(asList(superclass.getDeclaredFields()));
Type genericSuperclass = type.getGenericSuperclass();
Map<Type, Class<?>> typeVariables = new HashMap<>();
if (genericSuperclass instanceof ParameterizedType) {
Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments();
TypeVariable<? extends Class<?>>[] typeParameters = superclass.getTypeParameters();
for (int i = 0; i < actualTypeArguments.length; i++) {
if (actualTypeArguments[i] instanceof Class) {
typeVariables.put(typeParameters[i], (Class<?>) actualTypeArguments[i]);
}
}
}
inheritedFields.addAll(stream(superclass.getDeclaredFields())
.map(field -> new GenericField(field, typeVariables.getOrDefault(field.getGenericType(), field.getType())))
.collect(Collectors.toList()));
type = superclass;
}
return inheritedFields;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,66 @@ class Foo {
assertThat(foo.names.length).isNotEqualTo(foo.addresses.length);
}

@Test
void genericBaseClass() {
class Concrete extends GenericBaseClass<Integer> {
private final String y;

public Concrete(int x, String y) {
super(x);
this.y = y;
}

public String getY() {
return y;
}
}

Concrete concrete = easyRandom.nextObject(Concrete.class);
assertThat(concrete.getX().getClass()).isEqualTo(Integer.class);
assertThat(concrete.getY().getClass()).isEqualTo(String.class);
}

@Test
void genericBaseClassWithBean() {
class Concrete extends GenericBaseClass<Street> {
private final String y;

public Concrete(Street x, String y) {
super(x);
this.y = y;
}

public String getY() {
return y;
}
}

Concrete concrete = easyRandom.nextObject(Concrete.class);
assertThat(concrete.getX().getClass()).isEqualTo(Street.class);
assertThat(concrete.getY().getClass()).isEqualTo(String.class);
}

@Test
void boundedBaseClass() {
class Concrete extends BoundedBaseClass<BoundedBaseClass.IntWrapper> {
private final String y;

public Concrete(BoundedBaseClass.IntWrapper x, String y) {
super(x);
this.y = y;
}

public String getY() {
return y;
}
}

Concrete concrete = easyRandom.nextObject(Concrete.class);
assertThat(concrete.getX().getClass()).isEqualTo(BoundedBaseClass.IntWrapper.class);
assertThat(concrete.getY().getClass()).isEqualTo(String.class);
}

private void validatePerson(final Person person) {
assertThat(person).isNotNull();
assertThat(person.getEmail()).isNotEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

import javax.xml.bind.JAXBElement;

import org.jeasy.random.util.GenericField;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -87,7 +88,7 @@ void whenSkipRandomizerIsRegisteredForTheField_thenTheFieldShouldBeSkipped() thr
when(randomizerProvider.getRandomizerByField(name, context)).thenReturn(randomizer);

// When
fieldPopulator.populateField(human, name, context);
fieldPopulator.populateField(human, new GenericField(name), context);

// Then
assertThat(human.getName()).isNull();
Expand All @@ -103,7 +104,7 @@ void whenCustomRandomizerIsRegisteredForTheField_thenTheFieldShouldBePopulatedWi
when(randomizer.getRandomValue()).thenReturn(NAME);

// When
fieldPopulator.populateField(human, name, context);
fieldPopulator.populateField(human, new GenericField(name), context);

// Then
assertThat(human.getName()).isEqualTo(NAME);
Expand All @@ -119,7 +120,7 @@ void whenTheFieldIsOfTypeArray_thenShouldDelegatePopulationToArrayPopulator() th
when(arrayPopulator.getRandomArray(strings.getType(), context)).thenReturn(object);

// When
fieldPopulator.populateField(arrayBean, strings, context);
fieldPopulator.populateField(arrayBean, new GenericField(strings), context);

// Then
assertThat(arrayBean.getStrings()).isEqualTo(object);
Expand All @@ -134,7 +135,7 @@ void whenTheFieldIsOfTypeCollection_thenShouldDelegatePopulationToCollectionPopu
RandomizationContext context = new RandomizationContext(CollectionBean.class, new EasyRandomParameters());

// When
fieldPopulator.populateField(collectionBean, strings, context);
fieldPopulator.populateField(collectionBean, new GenericField(strings), context);

// Then
assertThat(collectionBean.getTypedCollection()).isEqualTo(persons);
Expand All @@ -149,7 +150,7 @@ void whenTheFieldIsOfTypeMap_thenShouldDelegatePopulationToMapPopulator() throws
RandomizationContext context = new RandomizationContext(MapBean.class, new EasyRandomParameters());

// When
fieldPopulator.populateField(mapBean, strings, context);
fieldPopulator.populateField(mapBean, new GenericField(strings), context);

// Then
assertThat(mapBean.getTypedMap()).isEqualTo(idToPerson);
Expand All @@ -164,7 +165,7 @@ void whenRandomizationDepthIsExceeded_thenFieldsAreNotInitialized() throws Excep
when(context.hasExceededRandomizationDepth()).thenReturn(true);

// When
fieldPopulator.populateField(human, name, context);
fieldPopulator.populateField(human, new GenericField(name), context);

// Then
assertThat(human.getName()).isNull();
Expand All @@ -179,7 +180,7 @@ void shouldFailWithNiceErrorMessageWhenUnableToCreateFieldValue() throws Excepti
JaxbElementFieldBean jaxbElementFieldBean = new JaxbElementFieldBean();
RandomizationContext context = Mockito.mock(RandomizationContext.class);

thenThrownBy(() -> { fieldPopulator.populateField(jaxbElementFieldBean, jaxbElementField, context); })
thenThrownBy(() -> { fieldPopulator.populateField(jaxbElementFieldBean, new GenericField(jaxbElementField), context); })
.hasMessage("Unable to create type: javax.xml.bind.JAXBElement for field: jaxbElementField of class: org.jeasy.random.FieldPopulatorTest$JaxbElementFieldBean");
}

Expand Down
Loading

0 comments on commit c376ec5

Please sign in to comment.