diff options
author | Carlo Zancanaro <carlo@zancanaro.id.au> | 2015-06-09 17:33:56 +1000 |
---|---|---|
committer | Carlo Zancanaro <carlo@zancanaro.id.au> | 2015-06-09 17:33:56 +1000 |
commit | dd9f72b94eb7b2c37061c80457e74e8d7ac3e18f (patch) | |
tree | 17ac650c0c4a5045b1cbf0ef5c194b0ea7f7acd3 | |
parent | 813e523e9e57dc38f81afc53340e216b948d87cf (diff) |
Add an ObjectGenerator<>, and related machinery (also a mapOf generator)
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(); + } +} |