summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlo Zancanaro <carlo@zancanaro.id.au>2015-06-09 17:33:56 +1000
committerCarlo Zancanaro <carlo@zancanaro.id.au>2015-06-09 17:33:56 +1000
commitdd9f72b94eb7b2c37061c80457e74e8d7ac3e18f (patch)
tree17ac650c0c4a5045b1cbf0ef5c194b0ea7f7acd3
parent813e523e9e57dc38f81afc53340e216b948d87cf (diff)
Add an ObjectGenerator<>, and related machinery (also a mapOf generator)
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/DataSourceHelper.java109
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/Generator.java1
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/Generators.java29
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/junit/Properties.java102
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java5
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/CharType.java15
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java117
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/IntRange.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/LongRange.java13
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java11
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java103
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java11
-rw-r--r--src/main/java/au/id/zancanaro/javacheck/state/CommandValue.java1
-rw-r--r--src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java1
-rw-r--r--src/test/java/au/id/zancanaro/javacheck/object/MyObject.java50
-rw-r--r--src/test/java/au/id/zancanaro/javacheck/object/MyObjectAddTest.java29
-rw-r--r--src/test/java/au/id/zancanaro/javacheck/object/SubObject.java44
18 files changed, 567 insertions, 100 deletions
diff --git a/src/main/java/au/id/zancanaro/javacheck/DataSourceHelper.java b/src/main/java/au/id/zancanaro/javacheck/DataSourceHelper.java
new file mode 100644
index 0000000..522fd26
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/DataSourceHelper.java
@@ -0,0 +1,109 @@
+package au.id.zancanaro.javacheck;
+
+import au.id.zancanaro.javacheck.annotations.DataSource;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.*;
+
+public class DataSourceHelper {
+ private final Class<?> classObject;
+ private final Map<Type, Generator<?>> generators;
+
+ public DataSourceHelper(Class<?> classObject) {
+ this.classObject = classObject;
+ this.generators = new HashMap<>();
+ }
+
+ public static Set<Type> validateGeneratorFields(Class<?> classObject, List<Throwable> errors) {
+ Set<Type> result = new HashSet<>();
+
+ for (Field field : classObject.getDeclaredFields()) {
+ if (field.isAnnotationPresent(DataSource.class)) {
+ boolean error = false;
+ if (!Modifier.isStatic(field.getModifiers())) {
+ errors.add(new Error("@DataSource field " + field.getName() + " must be static"));
+ error = true;
+ }
+ if (!Modifier.isPublic(field.getModifiers())) {
+ errors.add(new Error("@DataSource field " + field.getName() + " must be public"));
+ error = true;
+ }
+
+ Type type = field.getGenericType();
+ ParameterizedType parameterizedType;
+ if (type instanceof ParameterizedType) {
+ parameterizedType = (ParameterizedType) type;
+ if (parameterizedType.getRawType() instanceof Class) {
+ Class<?> c = (Class) parameterizedType.getRawType();
+ if (c == Generator.class) {
+ if (!error) {
+ result.add(parameterizedType.getActualTypeArguments()[0]);
+ }
+ } else {
+ errors.add(new Error("@DataSource fields must be of type Generator<T>"));
+ }
+ } else {
+ errors.add(new Error("@DataSource fields must be of type Generator<T>"));
+ }
+ } else {
+ errors.add(new Error("@DataSource fields must be of type Generator<T>"));
+ }
+ }
+ }
+
+ return result;
+ }
+
+
+ private static final Map<Type, Type> rawTypes;
+
+ static {
+ Map<Type, Type> types = new HashMap<>();
+ types.put(Double.class, Double.TYPE);
+ types.put(Float.class, Float.TYPE);
+ types.put(Long.class, Long.TYPE);
+ types.put(Integer.class, Integer.TYPE);
+ types.put(Short.class, Short.TYPE);
+ types.put(Byte.class, Byte.TYPE);
+ types.put(Character.class, Character.TYPE);
+ types.put(Boolean.class, Boolean.TYPE);
+ rawTypes = Collections.unmodifiableMap(types);
+ }
+
+ public Map<Type, Generator<?>> computeGenerators() {
+ if (generators.isEmpty()) {
+ for (Field field : classObject.getDeclaredFields()) {
+ if (!field.isAnnotationPresent(DataSource.class)) {
+ continue;
+ }
+ Type type = field.getGenericType();
+ if (!(type instanceof ParameterizedType)) {
+ continue;
+ }
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ if (!(parameterizedType.getRawType() instanceof Class)) {
+ continue;
+ }
+ Class<?> c = (Class) parameterizedType.getRawType();
+ if (c != Generator.class) {
+ continue;
+ }
+ try {
+ Type target = parameterizedType.getActualTypeArguments()[0];
+ @SuppressWarnings("unchecked")
+ Generator<Object> generator = (Generator<Object>) field.get(null);
+ generators.put(target, generator);
+ if (rawTypes.containsKey(target)) {
+ generators.put(rawTypes.get(target), generator);
+ }
+ } catch (IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ }
+ return generators;
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/Generator.java b/src/main/java/au/id/zancanaro/javacheck/Generator.java
index 24c23ca..ec7a16e 100644
--- a/src/main/java/au/id/zancanaro/javacheck/Generator.java
+++ b/src/main/java/au/id/zancanaro/javacheck/Generator.java
@@ -1,6 +1,5 @@
package au.id.zancanaro.javacheck;
-import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
diff --git a/src/main/java/au/id/zancanaro/javacheck/Generators.java b/src/main/java/au/id/zancanaro/javacheck/Generators.java
index f4ac025..63d4ece 100644
--- a/src/main/java/au/id/zancanaro/javacheck/Generators.java
+++ b/src/main/java/au/id/zancanaro/javacheck/Generators.java
@@ -1,8 +1,12 @@
package au.id.zancanaro.javacheck;
+import au.id.zancanaro.javacheck.object.GeneratorProvider;
+import au.id.zancanaro.javacheck.object.ObjectGenerator;
+
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -75,6 +79,7 @@ public final class Generators {
private static ShrinkStrategy<Long> longShrinkStrategy(final long bound) {
return value -> StreamSupport.stream(new Spliterators.AbstractSpliterator<Long>(Long.MAX_VALUE, Spliterator.ORDERED) {
long curr = value - bound;
+
@Override
public boolean tryAdvance(Consumer<? super Long> action) {
if (curr == 0) {
@@ -173,6 +178,22 @@ public final class Generators {
};
}
+ @SuppressWarnings("unchecked")
+ public static <K, V> Generator<Map<K, V>> mapOf(Generator<K> keyGen, Generator<V> valueGen) {
+ return (random, size) -> {
+ Generator<Integer> countGen = sized(s -> integer(0, s));
+ int count = countGen.generate(random, size).getValue();
+ return Generator.list(count, Generator.tuple(keyGen, valueGen))
+ .generate(random, size)
+ .map(pairs -> pairs.stream()
+ .collect(Collectors.toMap(
+ pair -> (K) pair.get(0),
+ pair -> (V) pair.get(1),
+ (first, second) -> second)))
+ .map(Collections::unmodifiableMap);
+ };
+ }
+
public static Generator<Character> character() {
return integer(0, 256).map(i -> (char) i.intValue());
}
@@ -216,4 +237,12 @@ public final class Generators {
return String.valueOf(chars);
});
}
+
+ public static <T> Generator<T> ofType(Class<T> type) {
+ return new ObjectGenerator<>(type);
+ }
+
+ public static <T> Generator<T> ofType(Class<T> type, GeneratorProvider provider) {
+ return new ObjectGenerator<>(type, provider);
+ }
}
diff --git a/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java b/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java
index 9d50af0..94032bc 100644
--- a/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java
+++ b/src/main/java/au/id/zancanaro/javacheck/junit/Properties.java
@@ -1,9 +1,9 @@
package au.id.zancanaro.javacheck.junit;
+import au.id.zancanaro.javacheck.DataSourceHelper;
import au.id.zancanaro.javacheck.Generator;
import au.id.zancanaro.javacheck.ShrinkResult;
import au.id.zancanaro.javacheck.ShrinkTree;
-import au.id.zancanaro.javacheck.annotations.DataSource;
import au.id.zancanaro.javacheck.annotations.Property;
import au.id.zancanaro.javacheck.annotations.Seed;
import org.junit.AssumptionViolatedException;
@@ -13,64 +13,26 @@ import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestClass;
-import java.lang.reflect.*;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
import java.util.*;
@SuppressWarnings("WeakerAccess")
public class Properties extends BlockJUnit4ClassRunner {
- private final Map<Type, Generator<?>> generators = new HashMap<>();
+ private final DataSourceHelper helper;
public Properties(Class<?> classObject) throws InitializationError {
super(classObject);
+ helper = new DataSourceHelper(classObject);
}
@Override
protected void collectInitializationErrors(List<Throwable> errors) {
super.collectInitializationErrors(errors);
- Set<Type> generated = validateGeneratorFields(errors);
+ Set<Type> generated = DataSourceHelper.validateGeneratorFields(getTestClass().getJavaClass(), errors);
validateTestMethodParameters(errors, generated);
}
- private Set<Type> validateGeneratorFields(List<Throwable> errors) {
- Set<Type> result = new HashSet<>();
- Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
-
- for (Field field : fields) {
- if (field.isAnnotationPresent(DataSource.class)) {
- boolean error = false;
- if (!Modifier.isStatic(field.getModifiers())) {
- errors.add(new Error("@DataSource field " + field.getName() + " must be static"));
- error = true;
- }
- if (!Modifier.isPublic(field.getModifiers())) {
- errors.add(new Error("@DataSource field " + field.getName() + " must be public"));
- error = true;
- }
-
- Type type = field.getGenericType();
- ParameterizedType parameterizedType;
- if (type instanceof ParameterizedType) {
- parameterizedType = (ParameterizedType) type;
- if (parameterizedType.getRawType() instanceof Class) {
- Class<?> c = (Class) parameterizedType.getRawType();
- if (c == Generator.class) {
- if (!error) {
- result.add(parameterizedType.getActualTypeArguments()[0]);
- }
- } else {
- errors.add(new Error("@DataSource fields must be of type Generator<T>"));
- }
- } else {
- errors.add(new Error("@DataSource fields must be of type Generator<T>"));
- }
- } else {
- errors.add(new Error("@DataSource fields must be of type Generator<T>"));
- }
- }
- }
- return result;
- }
-
private void validateTestMethodParameters(List<Throwable> errors, Set<Type> generated) {
for (FrameworkMethod each : computeTestMethods()) {
for (Type type : each.getMethod().getGenericParameterTypes()) {
@@ -82,56 +44,6 @@ public class Properties extends BlockJUnit4ClassRunner {
}
}
- private static final Map<Type, Type> rawTypes;
-
- static {
- Map<Type, Type> types = new HashMap<>();
- types.put(Double.class, Double.TYPE);
- types.put(Float.class, Float.TYPE);
- types.put(Long.class, Long.TYPE);
- types.put(Integer.class, Integer.TYPE);
- types.put(Short.class, Short.TYPE);
- types.put(Byte.class, Byte.TYPE);
- types.put(Character.class, Character.TYPE);
- types.put(Boolean.class, Boolean.TYPE);
- rawTypes = Collections.unmodifiableMap(types);
- }
-
- private Map<Type, Generator<?>> computeGenerators() {
- if (generators.isEmpty()) {
- Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
-
- for (Field field : fields) {
- if (!field.isAnnotationPresent(DataSource.class)) {
- continue;
- }
- Type type = field.getGenericType();
- if (!(type instanceof ParameterizedType)) {
- continue;
- }
- ParameterizedType parameterizedType = (ParameterizedType) type;
- if (!(parameterizedType.getRawType() instanceof Class)) {
- continue;
- }
- Class<?> c = (Class) parameterizedType.getRawType();
- if (c != Generator.class) {
- continue;
- }
- try {
- Type target = parameterizedType.getActualTypeArguments()[0];
- @SuppressWarnings("unchecked")
- Generator<Object> generator = (Generator<Object>) field.get(null);
- generators.put(target, generator);
- if (rawTypes.containsKey(target)) {
- generators.put(rawTypes.get(target), generator);
- }
- } catch (IllegalAccessException ex) {
- throw new RuntimeException(ex);
- }
- }
- }
- return generators;
- }
@Override
protected void validateConstructor(List<Throwable> errors) {
@@ -161,7 +73,7 @@ public class Properties extends BlockJUnit4ClassRunner {
@Override
public Statement methodBlock(final FrameworkMethod method) {
- return new GenerativeTester(method, getTestClass(), computeGenerators());
+ return new GenerativeTester(method, getTestClass(), helper.computeGenerators());
}
public static class GenerativeTester extends Statement {
diff --git a/src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java b/src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java
index 44c071b..d3747b9 100644
--- a/src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java
+++ b/src/main/java/au/id/zancanaro/javacheck/junit/PropertyError.java
@@ -4,6 +4,7 @@ import au.id.zancanaro.javacheck.ShrinkResult;
import java.util.Arrays;
import java.util.Iterator;
+import java.util.List;
@SuppressWarnings("WeakerAccess")
public class PropertyError extends AssertionError {
@@ -19,9 +20,9 @@ public class PropertyError extends AssertionError {
initCause(shrunk.thrown);
}
- private static String joinArgs(Object... params) {
+ private static String joinArgs(List<Object> params) {
StringBuilder sb = new StringBuilder();
- Iterator<Object> iterator = Arrays.asList(params).iterator();
+ Iterator<Object> iterator = params.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
sb.append(stringValueOf(next));
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/CharType.java b/src/main/java/au/id/zancanaro/javacheck/object/CharType.java
new file mode 100644
index 0000000..befbd04
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/CharType.java
@@ -0,0 +1,15 @@
+package au.id.zancanaro.javacheck.object;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CharType {
+ public static enum TYPE {
+ ALL, ASCII, ALPHA, ALPHA_NUMERIC
+ };
+ TYPE value();
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java b/src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java
new file mode 100644
index 0000000..ca7c9a9
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java
@@ -0,0 +1,13 @@
+package au.id.zancanaro.javacheck.object;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DoubleRange {
+ double min();
+ double max();
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java b/src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java
new file mode 100644
index 0000000..7e5964f
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java
@@ -0,0 +1,117 @@
+package au.id.zancanaro.javacheck.object;
+
+import au.id.zancanaro.javacheck.Generator;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static au.id.zancanaro.javacheck.Generators.*;
+
+public interface GeneratorProvider {
+ Generator<?> getGenerator(Type type, Annotation[] annotations, GeneratorProvider topLevel);
+
+ final static GeneratorProvider DEFAULT_PROVIDER = new GeneratorProvider() {
+ @SuppressWarnings("unchecked")
+ private <T> T getAnnotation(Annotation[] annotations, Class<T> type) {
+ for (Annotation ann : annotations) {
+ if (type.isAssignableFrom(ann.getClass())) {
+ return (T) ann;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Generator<?> getGenerator(Type type, Annotation[] annotations, GeneratorProvider provider) {
+ if (type == Integer.TYPE || type == Integer.class) {
+ IntRange range = getAnnotation(annotations, IntRange.class);
+ if (range == null) {
+ return integer();
+ } else {
+ return integer(range.min(), range.max());
+ }
+ } else if (type == Long.TYPE || type == Long.class) {
+ LongRange range = getAnnotation(annotations, LongRange.class);
+ if (range == null) {
+ return longInteger();
+ } else {
+ return longInteger(range.min(), range.max());
+ }
+ } else if (type == Double.TYPE || type == Double.class) {
+ DoubleRange range = getAnnotation(annotations, DoubleRange.class);
+ if (range == null) {
+ return doublePrecision();
+ } else {
+ return doublePrecision(range.min(), range.max());
+ }
+ } else if (type == String.class) {
+ CharType range = getAnnotation(annotations, CharType.class);
+ if (range == null) {
+ return string();
+ } else {
+ switch (range.value()) {
+ case ALPHA:
+ return stringOf(alphaCharacter());
+ case ALPHA_NUMERIC:
+ return stringOf(alphaNumericCharacter());
+ case ASCII:
+ return stringOf(asciiCharacter());
+ case ALL:
+ return stringOf(character());
+ default:
+ return string();
+ }
+ }
+ } else if (type == List.class) {
+ return listOf(provider.getGenerator(
+ List.class.getTypeParameters()[0],
+ new Annotation[0],
+ provider));
+ } else if (type == Map.class) {
+ TypeVariable<?>[] params = Map.class.getTypeParameters();
+ return mapOf(
+ provider.getGenerator(params[0], new Annotation[0], provider),
+ provider.getGenerator(params[1], new Annotation[0], provider));
+ } else if (type instanceof Class) {
+ return ofType((Class<?>) type, provider);
+ } else if (type instanceof ParameterizedType) {
+ ParameterizedType param = (ParameterizedType) type;
+ if (param.getRawType() instanceof Class) {
+ Class<?> container = (Class) param.getRawType();
+ TypeVariable<?>[] variables = container.getTypeParameters();
+ Type[] types = param.getActualTypeArguments();
+ return provider.getGenerator(
+ container,
+ new Annotation[0],
+ provider.withSubstitutedTypeVariables(variables, types));
+ }
+ throw new ObjectGenerationException("Cannot generate object of type " + type);
+ } else {
+ throw new ObjectGenerationException("Cannot generate object of type " + type);
+ }
+ }
+ };
+
+ default GeneratorProvider withSubstitutedTypeVariables(TypeVariable<?>[] variables, Type[] types) {
+ GeneratorProvider fallback = this;
+ return new GeneratorProvider() {
+ @Override
+ public Generator<?> getGenerator(Type type, Annotation[] annotations, GeneratorProvider provider) {
+ for (int i = 0; i < variables.length; ++i) {
+ if (Objects.equals(variables[i], type)) {
+ return provider.getGenerator(
+ types[i],
+ new Annotation[0],
+ this);
+ }
+ }
+ return fallback.getGenerator(type, annotations, provider);
+ }
+ };
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/IntRange.java b/src/main/java/au/id/zancanaro/javacheck/object/IntRange.java
new file mode 100644
index 0000000..8dc3f28
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/IntRange.java
@@ -0,0 +1,13 @@
+package au.id.zancanaro.javacheck.object;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface IntRange {
+ int max();
+ int min();
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/LongRange.java b/src/main/java/au/id/zancanaro/javacheck/object/LongRange.java
new file mode 100644
index 0000000..dd5a61f
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/LongRange.java
@@ -0,0 +1,13 @@
+package au.id.zancanaro.javacheck.object;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LongRange {
+ long max();
+ long min();
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java
new file mode 100644
index 0000000..d2147eb
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java
@@ -0,0 +1,11 @@
+package au.id.zancanaro.javacheck.object;
+
+public class ObjectGenerationException extends RuntimeException {
+ public ObjectGenerationException(String message) {
+ super(message);
+ }
+
+ public ObjectGenerationException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java
new file mode 100644
index 0000000..a31d990
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java
@@ -0,0 +1,103 @@
+package au.id.zancanaro.javacheck.object;
+
+import au.id.zancanaro.javacheck.Generator;
+import au.id.zancanaro.javacheck.ShrinkTree;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.*;
+import java.util.*;
+
+public class ObjectGenerator<T> implements Generator<T> {
+ private final Class<T> objectClass;
+ private final GeneratorProvider provider;
+ private final List<Generator<?>> constructorGenerators;
+ private final SortedMap<Field, Generator<?>> fieldGenerators;
+ private transient Constructor<T> constructor = null;
+
+ public ObjectGenerator(Class<T> objectClass) {
+ this(objectClass, GeneratorProvider.DEFAULT_PROVIDER);
+ }
+
+ public ObjectGenerator(Class<T> objectClass, GeneratorProvider provider) {
+ this.objectClass = objectClass;
+ this.provider = provider;
+ this.constructorGenerators = calculateConstructorGenerators();
+ this.fieldGenerators = calculateFieldGenerators();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Constructor<T> getConstructor() {
+ if (this.constructor == null) {
+ Constructor<?>[] constructors = objectClass.getConstructors();
+ if (constructors.length == 1) {
+ return (Constructor<T>) constructors[0];
+ } else {
+ for (Constructor<?> constructor : constructors) {
+ if (constructor.isAnnotationPresent(UseForGeneration.class)) {
+ if (this.constructor == null) {
+ this.constructor = (Constructor<T>) constructor;
+ } else {
+ throw new ObjectGenerationException("Multiple constructors are annotated with @UseForGeneration in class" + objectClass);
+ }
+ }
+ }
+ if (this.constructor == null) {
+ throw new ObjectGenerationException("Multiple constructors, but none are annotated with @UseForGeneration, in class" + objectClass);
+ }
+ }
+ }
+ return this.constructor;
+ }
+
+ private List<Generator<?>> calculateConstructorGenerators() {
+ List<Generator<?>> result = new ArrayList<>();
+ Constructor<T> constructor = getConstructor();
+ Type[] parameterTypes = constructor.getGenericParameterTypes();
+ Annotation[][] annotations = constructor.getParameterAnnotations();
+ for (int i = 0; i < parameterTypes.length; ++i) {
+ result.add(provider.getGenerator(parameterTypes[i], annotations[i], provider));
+ }
+ return result;
+ }
+
+ private SortedMap<Field, Generator<?>> calculateFieldGenerators() {
+ Comparator<Field> fieldComparator = Comparator.comparing(Field::getName);
+ SortedMap<Field, Generator<?>> result = new TreeMap<>(fieldComparator);
+ for (Field field : objectClass.getFields()) {
+ if (!Modifier.isFinal(field.getModifiers())
+ && !Modifier.isStatic(field.getModifiers())) {
+ result.put(field, provider.getGenerator(
+ field.getGenericType(),
+ field.getAnnotations(),
+ provider));
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public ShrinkTree<T> generate(Random random, int size) {
+ Generator<?>[] parameters = new Generator[constructorGenerators.size()];
+ parameters = constructorGenerators.toArray(parameters);
+ Generator<?>[] fields = new Generator[fieldGenerators.size()];
+ fields = fieldGenerators.values().toArray(fields);
+ return Generator.tuple(Generator.tuple(parameters), Generator.tuple(fields))
+ .generate(random, size)
+ .map(objs -> makeObject(
+ (List<?>) objs.get(0),
+ (List<?>) objs.get(1)));
+ }
+
+ private T makeObject(List<?> parameters, List<?> fields) {
+ try {
+ T obj = getConstructor().newInstance(parameters.toArray());
+ int i = 0;
+ for (Field key : fieldGenerators.keySet()) {
+ key.set(obj, fields.get(i++));
+ }
+ return obj;
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) {
+ throw new ObjectGenerationException(ex);
+ }
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java b/src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java
new file mode 100644
index 0000000..68990bf
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java
@@ -0,0 +1,11 @@
+package au.id.zancanaro.javacheck.object;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.CONSTRUCTOR)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UseForGeneration {
+}
diff --git a/src/main/java/au/id/zancanaro/javacheck/state/CommandValue.java b/src/main/java/au/id/zancanaro/javacheck/state/CommandValue.java
index 8584fc7..1ca51a9 100644
--- a/src/main/java/au/id/zancanaro/javacheck/state/CommandValue.java
+++ b/src/main/java/au/id/zancanaro/javacheck/state/CommandValue.java
@@ -2,7 +2,6 @@ package au.id.zancanaro.javacheck.state;
import java.util.Map;
import java.util.NoSuchElementException;
-import java.util.function.Supplier;
public class CommandValue<T> {
public static interface Action<T> {
diff --git a/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java b/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java
index 70b2425..fcc8baf 100644
--- a/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java
+++ b/src/test/java/au/id/zancanaro/javacheck/SimpleListOperationsTest.java
@@ -6,7 +6,6 @@ import au.id.zancanaro.javacheck.junit.Properties;
import org.junit.runner.RunWith;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import static au.id.zancanaro.javacheck.Generators.*;
diff --git a/src/test/java/au/id/zancanaro/javacheck/object/MyObject.java b/src/test/java/au/id/zancanaro/javacheck/object/MyObject.java
new file mode 100644
index 0000000..ac0c370
--- /dev/null
+++ b/src/test/java/au/id/zancanaro/javacheck/object/MyObject.java
@@ -0,0 +1,50 @@
+package au.id.zancanaro.javacheck.object;
+
+public class MyObject {
+ public final String string;
+ public final int value;
+ public final SubObject<Integer> subObject;
+
+ public MyObject(String string, int value, SubObject<Integer> subObject) {
+ this.string = string;
+ this.value = value;
+ this.subObject = subObject;
+ }
+
+ @UseForGeneration
+ public MyObject(String string, SubObject<Integer> subObject) {
+ this(string, string.length(), subObject);
+ }
+
+ public MyObject add(MyObject other) {
+ return new MyObject(
+ this.string + other.string,
+ this.value + other.value,
+ this.subObject.add(other.subObject));
+ }
+
+ @Override
+ public String toString() {
+ return "{" + string + ", " + value + ", " + subObject + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ MyObject myObject = (MyObject) o;
+
+ return value == myObject.value
+ && string.equals(myObject.string)
+ && subObject.equals(myObject.subObject);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = string.hashCode();
+ result = 31 * result + value;
+ result = 31 * result + subObject.hashCode();
+ return result;
+ }
+}
diff --git a/src/test/java/au/id/zancanaro/javacheck/object/MyObjectAddTest.java b/src/test/java/au/id/zancanaro/javacheck/object/MyObjectAddTest.java
new file mode 100644
index 0000000..7965449
--- /dev/null
+++ b/src/test/java/au/id/zancanaro/javacheck/object/MyObjectAddTest.java
@@ -0,0 +1,29 @@
+package au.id.zancanaro.javacheck.object;
+
+import au.id.zancanaro.javacheck.Generator;
+import au.id.zancanaro.javacheck.annotations.DataSource;
+import au.id.zancanaro.javacheck.annotations.Property;
+import au.id.zancanaro.javacheck.junit.Properties;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+import static au.id.zancanaro.javacheck.Generators.ofType;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Properties.class)
+public class MyObjectAddTest {
+
+ @DataSource
+ public static Generator<MyObject> source = ofType(MyObject.class);
+
+ @Property
+ public void testAdd(MyObject a, MyObject b) {
+ MyObject added = a.add(b);
+ assertTrue(added.string.length() == a.string.length() + b.string.length());
+ assertTrue(added.value == a.value + b.value);
+ assertEquals(added.subObject, a.subObject.add(b.subObject));
+ }
+
+}
diff --git a/src/test/java/au/id/zancanaro/javacheck/object/SubObject.java b/src/test/java/au/id/zancanaro/javacheck/object/SubObject.java
new file mode 100644
index 0000000..bdf2ce8
--- /dev/null
+++ b/src/test/java/au/id/zancanaro/javacheck/object/SubObject.java
@@ -0,0 +1,44 @@
+package au.id.zancanaro.javacheck.object;
+
+import java.util.*;
+
+public class SubObject<T> {
+ public final Map<String, List<T>> obj;
+
+ public SubObject(Map<String, List<T>> obj) {
+ this.obj = obj;
+ }
+
+ public SubObject<T> add(SubObject<T> other) {
+ Map<String, List<T>> values = new HashMap<>(obj);
+ for (Map.Entry<String, List<T>> entry : other.obj.entrySet()) {
+ String key = entry.getKey();
+ if (values.containsKey(key)) {
+ List<T> result = new ArrayList<>(values.get(key));
+ result.addAll(entry.getValue());
+ values.put(key, Collections.unmodifiableList(result));
+ } else {
+ values.put(key, entry.getValue());
+ }
+ }
+ return new SubObject<>(Collections.unmodifiableMap(values));
+ }
+
+ @Override
+ public String toString() {
+ return obj.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ SubObject subObject = (SubObject) o;
+ return obj.equals(subObject.obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return obj.hashCode();
+ }
+}