From dd9f72b94eb7b2c37061c80457e74e8d7ac3e18f Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Tue, 9 Jun 2015 17:33:56 +1000 Subject: Add an ObjectGenerator<>, and related machinery (also a mapOf generator) --- .../id/zancanaro/javacheck/DataSourceHelper.java | 109 +++++++++++++++++++ .../java/au/id/zancanaro/javacheck/Generator.java | 1 - .../java/au/id/zancanaro/javacheck/Generators.java | 29 +++++ .../id/zancanaro/javacheck/junit/Properties.java | 102 ++---------------- .../zancanaro/javacheck/junit/PropertyError.java | 5 +- .../au/id/zancanaro/javacheck/object/CharType.java | 15 +++ .../id/zancanaro/javacheck/object/DoubleRange.java | 13 +++ .../javacheck/object/GeneratorProvider.java | 117 +++++++++++++++++++++ .../au/id/zancanaro/javacheck/object/IntRange.java | 13 +++ .../id/zancanaro/javacheck/object/LongRange.java | 13 +++ .../object/ObjectGenerationException.java | 11 ++ .../javacheck/object/ObjectGenerator.java | 103 ++++++++++++++++++ .../javacheck/object/UseForGeneration.java | 11 ++ .../id/zancanaro/javacheck/state/CommandValue.java | 1 - .../javacheck/SimpleListOperationsTest.java | 1 - .../au/id/zancanaro/javacheck/object/MyObject.java | 50 +++++++++ .../javacheck/object/MyObjectAddTest.java | 29 +++++ .../id/zancanaro/javacheck/object/SubObject.java | 44 ++++++++ 18 files changed, 567 insertions(+), 100 deletions(-) create mode 100644 src/main/java/au/id/zancanaro/javacheck/DataSourceHelper.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/CharType.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/DoubleRange.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/GeneratorProvider.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/IntRange.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/LongRange.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerationException.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/ObjectGenerator.java create mode 100644 src/main/java/au/id/zancanaro/javacheck/object/UseForGeneration.java create mode 100644 src/test/java/au/id/zancanaro/javacheck/object/MyObject.java create mode 100644 src/test/java/au/id/zancanaro/javacheck/object/MyObjectAddTest.java create mode 100644 src/test/java/au/id/zancanaro/javacheck/object/SubObject.java 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> generators; + + public DataSourceHelper(Class classObject) { + this.classObject = classObject; + this.generators = new HashMap<>(); + } + + public static Set validateGeneratorFields(Class classObject, List errors) { + Set 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")); + } + } else { + errors.add(new Error("@DataSource fields must be of type Generator")); + } + } else { + errors.add(new Error("@DataSource fields must be of type Generator")); + } + } + } + + return result; + } + + + private static final Map rawTypes; + + static { + Map 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> 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 generator = (Generator) 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 longShrinkStrategy(final long bound) { return value -> StreamSupport.stream(new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.ORDERED) { long curr = value - bound; + @Override public boolean tryAdvance(Consumer action) { if (curr == 0) { @@ -173,6 +178,22 @@ public final class Generators { }; } + @SuppressWarnings("unchecked") + public static Generator> mapOf(Generator keyGen, Generator valueGen) { + return (random, size) -> { + Generator 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() { return integer(0, 256).map(i -> (char) i.intValue()); } @@ -216,4 +237,12 @@ public final class Generators { return String.valueOf(chars); }); } + + public static Generator ofType(Class type) { + return new ObjectGenerator<>(type); + } + + public static Generator ofType(Class 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> generators = new HashMap<>(); + private final DataSourceHelper helper; public Properties(Class classObject) throws InitializationError { super(classObject); + helper = new DataSourceHelper(classObject); } @Override protected void collectInitializationErrors(List errors) { super.collectInitializationErrors(errors); - Set generated = validateGeneratorFields(errors); + Set generated = DataSourceHelper.validateGeneratorFields(getTestClass().getJavaClass(), errors); validateTestMethodParameters(errors, generated); } - private Set validateGeneratorFields(List errors) { - Set 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")); - } - } else { - errors.add(new Error("@DataSource fields must be of type Generator")); - } - } else { - errors.add(new Error("@DataSource fields must be of type Generator")); - } - } - } - return result; - } - private void validateTestMethodParameters(List errors, Set generated) { for (FrameworkMethod each : computeTestMethods()) { for (Type type : each.getMethod().getGenericParameterTypes()) { @@ -82,56 +44,6 @@ public class Properties extends BlockJUnit4ClassRunner { } } - private static final Map rawTypes; - - static { - Map 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> 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 generator = (Generator) 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 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 params) { StringBuilder sb = new StringBuilder(); - Iterator iterator = Arrays.asList(params).iterator(); + Iterator 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 getAnnotation(Annotation[] annotations, Class 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 implements Generator { + private final Class objectClass; + private final GeneratorProvider provider; + private final List> constructorGenerators; + private final SortedMap> fieldGenerators; + private transient Constructor constructor = null; + + public ObjectGenerator(Class objectClass) { + this(objectClass, GeneratorProvider.DEFAULT_PROVIDER); + } + + public ObjectGenerator(Class objectClass, GeneratorProvider provider) { + this.objectClass = objectClass; + this.provider = provider; + this.constructorGenerators = calculateConstructorGenerators(); + this.fieldGenerators = calculateFieldGenerators(); + } + + @SuppressWarnings("unchecked") + private Constructor getConstructor() { + if (this.constructor == null) { + Constructor[] constructors = objectClass.getConstructors(); + if (constructors.length == 1) { + return (Constructor) constructors[0]; + } else { + for (Constructor constructor : constructors) { + if (constructor.isAnnotationPresent(UseForGeneration.class)) { + if (this.constructor == null) { + this.constructor = (Constructor) 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> calculateConstructorGenerators() { + List> result = new ArrayList<>(); + Constructor 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> calculateFieldGenerators() { + Comparator fieldComparator = Comparator.comparing(Field::getName); + SortedMap> 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 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 { public static interface Action { 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 subObject; + + public MyObject(String string, int value, SubObject subObject) { + this.string = string; + this.value = value; + this.subObject = subObject; + } + + @UseForGeneration + public MyObject(String string, SubObject 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 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 { + public final Map> obj; + + public SubObject(Map> obj) { + this.obj = obj; + } + + public SubObject add(SubObject other) { + Map> values = new HashMap<>(obj); + for (Map.Entry> entry : other.obj.entrySet()) { + String key = entry.getKey(); + if (values.containsKey(key)) { + List 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(); + } +} -- cgit v1.2.3