diff --git a/.gitignore b/.gitignore index fcd506e..d2705fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ target/ Antidote.iml .gradle/ build/ -gen/ \ No newline at end of file +gen/ +out/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3ab2ede..909a14d 100644 --- a/build.gradle +++ b/build.gradle @@ -37,16 +37,20 @@ repositories { } dependencies { - // This dependency is exported to consumers, that is to say found on their compile classpath. -// api 'org.apache.commons:commons-math3:3.6.1' + // advanced reflection utilities + implementation 'io.leangen.geantyref:geantyref:1.3.4' // This dependency is used internally, and not exposed to consumers on their own compile classpath. - compile 'com.google.guava:guava:21.0' + implementation 'com.google.guava:guava:21.0' // Use JUnit test framework api 'junit:junit:4.12' } +compileJava.options.compilerArgs.add '-parameters' +compileTestJava.options.compilerArgs.add '-parameters' + + /** * Publishing to maven central: * diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c..9fa57af 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun May 26 12:58:48 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a21117e --- /dev/null +++ b/readme.md @@ -0,0 +1,343 @@ +# Java SmallCheck + +Java SmallCheck is a Java library for property based testing. +It will generate all possible inputs (up to a certain size) for testing your functions. + +This library is based on ideas from "Smallcheck and lazy smallcheck: automatic exhaustive testing for small values" (Colin Runciman, Matthew Naylor, and Matthew Naylor; Haskell 2008) + + +## Basic Example + +The following function is supposed to compute the maximum out of 3 integers. +Unfortunately it contains a bug. +Can you find a counter example where it would fail? + + static int max3(int x, int y, int z) { + if (x > y && x > z) { + return x; + } else if (y > x && y > z) { + return y; + } else { + return z; + } + } + +With SmallCheck it is easy to write a test that finds a minimal counter example: + + + import org.junit.runner.RunWith; + import smallcheck.SmallCheckRunner; + import smallcheck.annotations.Property; + + import static org.junit.Assert.assertTrue; + + @RunWith(SmallCheckRunner.class) + public class Max3Example { + + @Property + public void testMax3(int x, int y, int z) { + int result = Max.max3(x, y, z); + assertTrue(result >= x); + assertTrue(result >= y); + assertTrue(result >= z); + } + + } + +When executed this test produces the following counter example: + + java.lang.AssertionError: Test failed when calling testMax3 with the following arguments: + x = 1 + y = 1 + z = 0 + at org.junit.Assert.fail(Assert.java:86) + at org.junit.Assert.assertTrue(Assert.java:41) + at org.junit.Assert.assertTrue(Assert.java:52) + at Max3Example.testMax3(Max3Example.java:26) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.base/java.lang.reflect.Method.invoke(Method.java:566) + + + +## Getting Started + +We recommend using [Gradle](https://gradle.org/) as the build tool. +If you are using a different build tool, follow the instructions at [https://jitpack.io/#peterzeller/java-smallcheck](https://jitpack.io/#peterzeller/java-smallcheck). + + +For Gradle follow these steps: + + + +1. Add Jitpack to your repositories in `build.gradle`: + + repositories { + jcenter() + maven { url 'https://jitpack.io' } + } + +2. Add the SmallCheck dependency in `build.gradle`: + + dependencies { + // Smallcheck Testing library + testCompile group: 'com.github.peterzeller', name: 'java-smallcheck', version: '-SNAPSHOT' + } + + Change `-SNAPSHOT` to a fixed tag to get a stable build. + +3. (optional) Configure Gradle to keep parameter names. This will give you better error messages. + + compileTestJava.options.compilerArgs.add '-parameters' + + +## Writing Property Tests + +To write property based tests using SmallCheck, two ingredients are required: + +1. The class containing the tests must be annotated with + + @RunWith(SmallCheckRunner.class) + +2. Each property is written in a public method annotated with `@Property`, similar to the `@Test` annotation for normal Junit tests. + + +SmallCheck will invoke each property method with all possible parameters up to a certain depth. + + @Property + public void generate(int x, char c) { + System.out.println(x + ", " + c); + } + +This simple example will be invoked with the following inputs: + + // depth 1 + 0, a + 1, a + -1, a + 0, b + 1, b + -1, b + // depth 2 + 0, a + 1, a + 2, a + -1, a + -2, a + 0, b + 1, b + 2, b + -1, b + -2, b + 0, c + 1, c + 2, c + -1, c + -2, c + // depth 3 + 0, a + 1, a + 2, a + 3, a + -1, a + -2, a + -3, a + 0, b + 1, b + 2, b + 3, b + -1, b + -2, b + -3, b + 0, c + 1, c + 2, c + 3, c + -1, c + -2, c + -3, c + 0, d + 1, d + 2, d + 3, d + -1, d + -2, d + -3, d + // depth 4 + ... + +By default values up to depth 5 will be tested. + +Many standard Java types (like `int` and `char` above) are supported with built-in generators. +For other types, custom generators can be written. + + + +### Property Settings + +The `@Property` annotation provides the following parameters for configuring SmallCheck: + +- **int maxDepth** (default 5) + + The maximum depth for generated inputs. + +- **int maxInvocations** (default 100000) + + The maximum number of different inputs used in testing. + +- **long minExamples** (default 20) + + The minimum number of valid examples that must be produced. + The test will fail if not enough generated arguments pass the preconditions giving with JUnits `Asssume` methods. + +- **int timeout** (default -1) + + A maximum execution time in seconds. + The test fails if the overall execution takes longer than the given duration. + + +The following example shows a property with custom configuration: + + @Property(maxDepth = 6, maxInvocations = 1000, minExamples = 100, timeout = 30) + public void configuredExample(int x, char c) { + ... + } + + +## Custom Generators + +A generator for a type `T` is defined by creating a subclass of `SeriesGen`: + + public abstract class SeriesGen { + + public abstract Stream generate(int depth); + + public T copy(T obj) { + return obj; + } + } + +The `generate` method creates a stream of elements up to a certain depth. + +For example the builtin `IntegerGen` generates values the numbers `0, 1, 2, 3, -1, -2, -3` for `depth = 3`. + +The `copy` method must be overridden for mutable datatypes. +It should create a copy of a `T` object. +This is used when generating collections like a `List`. +The List generator must generate many lists starting with the same element and thus needs to call `copy`. + +The following example shows a custom number generator that only generates even numbers. + + + public static class CustomNumberGen extends SeriesGen { + @Override + public Stream generate(int depth) { + return IntStream.range(0, 1 + depth).map(i -> 2 * i).boxed(); + } + } + + public static class CustomCharGen extends SeriesGen { + @Override + public Stream generate(int depth) { + return IntStream.range(0, 1+depth).mapToObj((int i) -> { + if (i % 2 == 0) { + return (char) ('a' + i / 2); + } else { + return (char) ('A' + i / 2); + } + }); + } + } + + +### Registering Custom Generators + +To use the custom generators they can be registered for a test method using the `@RegisterGenerator` annotation as shown in the example below. + + + @Property + @RegisterGenerator(CustomNumberGen.class) + public void genList(List list) { + System.out.println(list); + } + + @Property + @RegisterGenerator(CustomNumberGen.class) + @RegisterGenerator(CustomCharGen.class) + public void genMap(Map m) { + System.out.println(m); + } + + +### Configuring Custom Generators with @From + +It is also possible to use the `@From` annotation on a specific parameter to use the generator only for that case: + + @Property + public void genList2(List<@From(CustomNumberGen.class) Integer> list) { + System.out.println(list); + } + +### Custom Generators from Static Factories + +Another option for generating custom values is to define a static factory. +A static factory is a class that provides static methods that generate instances of a certain type. + +SmallCheck will try all combinations of these methods to create values of the type. + +In the example below we use this feature to create arithmetic expressions and find the smallest expression that evaluates to a value greater than or equal to 8. +The expression found is `((2 * 2) * 2)`. + + + @Property(maxInvocations = 5000000) + @StaticFactory(ExprFactory.class) + public void testExpr(Expr e) { + assertTrue(e.evaluate() < 8); + } + + + public static class ExprFactory { + public static Number number(int i) { + return new Number(i); + } + + public static Plus plus(Expr a, Expr b) { + return new Plus(a, b); + } + + public static Mult mult(Expr a, Expr b) { + return new Mult(a, b); + } + } + + + +## The StateGen Generator + +So far we have only seen SmallCheck generate inputs in the form of parameters. +The stateful generator `StateGen` provides an alternative way to generate inputs for your tests. + +To use this, simply add a parameter of type `StateGen` to your test method and use the `StateGen.gen` methods to generate values. + + @Property + public void testMax3S(StateGen sg) { + int x = sg.gen(Integer.class); + int y = sg.gen(Integer.class); + int z = sg.gen(Integer.class); + int result = max3(x, y, z); + assertTrue(result >= x); + assertTrue(result >= y); + assertTrue(result >= z); + } + +SmallCheck will invoke the test method multiple times and choose different values each execution. + +First all values for `z` (the last choice) are tested. +When all values for `z` are ok, SmallCheck will backtrack and try the next value for `y`. + +Since the last generated values are explored first, the counter examples given with `StateGen` differ from the ones found with normal parameters. + + + + diff --git a/src/main/java/smallcheck/PropertyStatement.java b/src/main/java/smallcheck/PropertyStatement.java index 21f4403..23c66a4 100644 --- a/src/main/java/smallcheck/PropertyStatement.java +++ b/src/main/java/smallcheck/PropertyStatement.java @@ -5,11 +5,8 @@ import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; -import smallcheck.annotations.Property; -import smallcheck.annotations.StaticFactories; -import smallcheck.annotations.StaticFactory; -import smallcheck.generators.GenFactory; -import smallcheck.generators.ParamGen; +import smallcheck.annotations.*; +import smallcheck.generators.*; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; @@ -28,6 +25,8 @@ public class PropertyStatement extends Statement { private final FrameworkMethod method; private final GenFactory genFactory; private final Object testInstance; + private static ThreadLocal localGenFactory = new ThreadLocal<>(); + private static ThreadLocal localDepth = new ThreadLocal<>(); public PropertyStatement(Property property, FrameworkMethod method, TestClass testClass) { this.property = property; @@ -41,6 +40,13 @@ public PropertyStatement(Property property, FrameworkMethod method, TestClass te } else if (annotation instanceof StaticFactory) { StaticFactory staticFactory = (StaticFactory) annotation; genFactory.addStaticFactory(staticFactory.value(), staticFactory.copyFunc()); + } else if (annotation instanceof RegisterGenerators) { + for (RegisterGenerator registerGenerator : ((RegisterGenerators) annotation).value()) { + genFactory.registerGenerator(registerGenerator.value()); + } + } else if (annotation instanceof RegisterGenerator) { + RegisterGenerator registerGenerator = (RegisterGenerator) annotation; + genFactory.registerGenerator(registerGenerator.value()); } } @@ -93,30 +99,71 @@ public void evaluate() throws Throwable { } private void execute(Method m, Parameter[] parameters, AtomicLong invocations, AtomicLong preConditionFailures, int maxDepth, int maxInvocations, AtomicReference lastArgs) { + localGenFactory.set(genFactory); for (int depth = 0; depth <= maxDepth; depth++) { + localDepth.set(depth); Stream argStream = ParamGen.generate(genFactory, parameters, depth); argStream.forEach(args -> { lastArgs.set(args); - try { - if (invocations.incrementAndGet() > maxInvocations) { - throw new MaxInvocationsReached(); + + StateGen stateGen = null; + for (Object arg : args) { + if (arg instanceof StateGen) { + if (stateGen != null) { + throw new IllegalArgumentException("Only one StateGen argument is allowed."); + } + stateGen = (StateGen) arg; } - m.invoke(testInstance, args); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof AssumptionViolatedException) { - preConditionFailures.incrementAndGet(); - // ignore this case - return; + } + + while (true) { + try { + if (invocations.incrementAndGet() > maxInvocations) { + throw new MaxInvocationsReached(); + } + m.invoke(testInstance, args); + if (stateGen == null) { + return; + } else { + stateGen.restart(); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof AssumptionViolatedException) { + preConditionFailures.incrementAndGet(); + if (stateGen == null) { + // ignore this case + return; + } else { + stateGen.restart(); + } + } else if (cause instanceof RestartException) { + RestartException restartException = (RestartException) cause; + if (stateGen == null || restartException.getStackDepth() == 0) { + // completed all invocations + return; + } else { + stateGen.restart(restartException.getStackDepth() - 1); + } + } else { + throw new SmallcheckException(m, args, cause); + } } - throw new SmallcheckException(m, args, cause); } }); } } + public static GenFactory getLocalGenFactory() { + return localGenFactory.get(); + } + + public static int getLocalDepth() { + return localDepth.get(); + } + private class MaxInvocationsReached extends RuntimeException { } } diff --git a/src/main/java/smallcheck/annotations/RegisterGenerator.java b/src/main/java/smallcheck/annotations/RegisterGenerator.java new file mode 100644 index 0000000..d0b8ca8 --- /dev/null +++ b/src/main/java/smallcheck/annotations/RegisterGenerator.java @@ -0,0 +1,19 @@ +package smallcheck.annotations; + +import smallcheck.generators.SeriesGen; + +import java.lang.annotation.*; +import java.lang.reflect.Type; +import java.util.function.Function; + +/** + * + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(RegisterGenerators.class) +public @interface RegisterGenerator { + + Class> value(); + +} diff --git a/src/main/java/smallcheck/annotations/RegisterGenerators.java b/src/main/java/smallcheck/annotations/RegisterGenerators.java new file mode 100644 index 0000000..f405f20 --- /dev/null +++ b/src/main/java/smallcheck/annotations/RegisterGenerators.java @@ -0,0 +1,17 @@ +package smallcheck.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface RegisterGenerators { + + RegisterGenerator[] value(); + +} diff --git a/src/main/java/smallcheck/generators/CharGenCase.java b/src/main/java/smallcheck/generators/CharGenCase.java new file mode 100644 index 0000000..0f472ec --- /dev/null +++ b/src/main/java/smallcheck/generators/CharGenCase.java @@ -0,0 +1,34 @@ +package smallcheck.generators; + +import com.google.common.collect.Streams; + +import java.util.Iterator; +import java.util.stream.Stream; + +/** + * Generates lower and upper case letters + * 'a', 'A', 'b', 'B', ... + */ +public class CharGenCase extends SeriesGen { + @Override + public Stream generate(int depth) { + return Streams.stream(new Iterator() { + int i = -1; + + @Override + public boolean hasNext() { + return i < depth; + } + + @Override + public Character next() { + i++; + if (i % 2 == 0) { + return (char) ('a' + i / 2); + } else { + return (char) ('A' + i / 2); + } + } + }); + } +} diff --git a/src/main/java/smallcheck/generators/GenFactory.java b/src/main/java/smallcheck/generators/GenFactory.java index 65c626b..333cc4f 100644 --- a/src/main/java/smallcheck/generators/GenFactory.java +++ b/src/main/java/smallcheck/generators/GenFactory.java @@ -1,23 +1,27 @@ package smallcheck.generators; +import io.leangen.geantyref.GenericTypeReflector; import smallcheck.annotations.From; import java.lang.reflect.*; import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * */ public class GenFactory { - private Map> typeGenerators = initDefaultGenerators(); - private List staticFactories = new ArrayList<>(); + private final Map>> typeGeneratorClasses = new HashMap<>(); + private final Map> typeGenerators = new HashMap<>(); + private final List staticFactories = new ArrayList<>(); private static class StaticFactory { Class clazz; - Function copyMethod; + Function copyMethod; public StaticFactory(Class clazz, Function copyMethod) { this.clazz = clazz; @@ -25,28 +29,30 @@ public StaticFactory(Class clazz, Function copyMethod) { } } + public GenFactory() { + initDefaultGenerators(); + typeGeneratorClasses.put(String.class, StringGen.class); + } - private Map> initDefaultGenerators() { - HashMap> res = new HashMap<>(); - res.put(int.class, new IntegerGen()); - res.put(Integer.class, new IntegerGen()); - res.put(long.class, new LongGen()); - res.put(Long.class, new LongGen()); - res.put(boolean.class, new BoolGen()); - res.put(Boolean.class, new BoolGen()); - res.put(byte.class, new ByteGen()); - res.put(Byte.class, new ByteGen()); - res.put(char.class, new CharGen()); - res.put(Character.class, new CharGen()); - res.put(short.class, new ShortGen()); - res.put(Short.class, new ShortGen()); - res.put(boolean[].class, ArrayGenG.booleanArray(new BoolGen())); - res.put(byte[].class, ArrayGenG.byteArray(new ByteGen())); - res.put(int[].class, ArrayGenG.intArray(new IntegerGen())); - res.put(long[].class, ArrayGenG.longArray(new LongGen())); - res.put(char[].class, ArrayGenG.charArray(new CharGen())); - res.put(short[].class, ArrayGenG.shortArray(new ShortGen())); - return res; + private void initDefaultGenerators() { + typeGenerators.put(int.class, new IntegerGen()); + typeGenerators.put(Integer.class, new IntegerGen()); + typeGenerators.put(long.class, new LongGen()); + typeGenerators.put(Long.class, new LongGen()); + typeGenerators.put(boolean.class, new BoolGen()); + typeGenerators.put(Boolean.class, new BoolGen()); + typeGenerators.put(byte.class, new ByteGen()); + typeGenerators.put(Byte.class, new ByteGen()); + typeGenerators.put(char.class, new CharGen()); + typeGenerators.put(Character.class, new CharGen()); + typeGenerators.put(short.class, new ShortGen()); + typeGenerators.put(Short.class, new ShortGen()); + typeGenerators.put(boolean[].class, ArrayGenG.booleanArray(new BoolGen())); + typeGenerators.put(byte[].class, ArrayGenG.byteArray(new ByteGen())); + typeGenerators.put(int[].class, ArrayGenG.intArray(new IntegerGen())); + typeGenerators.put(long[].class, ArrayGenG.longArray(new LongGen())); + typeGenerators.put(char[].class, ArrayGenG.charArray(new CharGen())); + typeGenerators.put(short[].class, ArrayGenG.shortArray(new ShortGen())); } @@ -62,6 +68,30 @@ public SeriesGen genForType(AnnotatedType annotatedType) { } } else if (typeGenerators.containsKey(type)) { return typeGenerators.get(type); + } else if (typeGeneratorClasses.containsKey(type)) { + Class> c = typeGeneratorClasses.get(type); + + List> validConstructors = Arrays.stream(c.getConstructors()) + .filter(constr -> Arrays.stream(constr.getParameterTypes()).allMatch( + SeriesGen.class::isAssignableFrom + )) + .sorted(Comparator.comparing((Constructor constr) -> constr.getParameterTypes().length).reversed()) + .collect(Collectors.toList()); + if (validConstructors.size() == 0) { + throw new RuntimeException("Cannot instantiate " + c +" for type " + type + ".\n" + + "Constructor which only takes other SeriesGens required."); + } + Constructor constr = validConstructors.get(0); + Object[] args = Arrays.stream(constr.getGenericParameterTypes()) + .map(this::genForType) + .toArray(); + try { + SeriesGen res = (SeriesGen) constr.newInstance(args); + typeGenerators.put(type, res); + return res; + } catch (Exception e) { + throw new RuntimeException("Could not create Generator for type " + type, e); + } } else if (type.equals(String.class)) { return new StringGen(); } else if (annotatedType instanceof AnnotatedArrayType) { @@ -94,37 +124,11 @@ public SeriesGen genForType(AnnotatedType annotatedType) { } if (type instanceof Class) { - Class clazz = (Class) type; - - - - // try to find static factory methods - List staticFactoryMethods = new ArrayList<>(); - Function copyFunc = null; - for (StaticFactory staticFactory : staticFactories) { - for (Method method : staticFactory.clazz.getMethods()) { - if (clazz.isAssignableFrom(method.getReturnType())) { - staticFactoryMethods.add(method); - copyFunc = staticFactory.copyMethod; - } - } - } - if (!staticFactoryMethods.isEmpty()) { - SeriesGen gen = new StaticFactoryMethodsGenerator(this, staticFactoryMethods, copyFunc); - typeGenerators.put(clazz, gen); - return gen; - } - - if (Enum.class.isAssignableFrom(clazz)) { - // we have an enum: - return new EnumGen(clazz); - } + SeriesGen clazz = genFor((Class) type); + if (clazz != null) return clazz; } - - - String msg = "Could not find generator for type " + type; msg += " (" + type.getClass() + ")"; if (annotatedType.getAnnotations().length > 0) { @@ -134,6 +138,44 @@ public SeriesGen genForType(AnnotatedType annotatedType) { } + public SeriesGen genForType(Type type) { + return genForType(GenericTypeReflector.annotate(type)); + } + + private SeriesGen genFor(Class type) { + if (StateGen.class.isAssignableFrom(type)) { + return new SeriesGen() { + @Override + public Stream generate(int depth) { + return Stream.of(new StateGen(GenFactory.this, depth)); + } + }; + } + + // try to find static factory methods + List staticFactoryMethods = new ArrayList<>(); + Function copyFunc = null; + for (StaticFactory staticFactory : staticFactories) { + for (Method method : staticFactory.clazz.getMethods()) { + if (type.isAssignableFrom(method.getReturnType())) { + staticFactoryMethods.add(method); + copyFunc = staticFactory.copyMethod; + } + } + } + if (!staticFactoryMethods.isEmpty()) { + SeriesGen gen = new StaticFactoryMethodsGenerator(this, staticFactoryMethods, copyFunc); + typeGenerators.put(type, gen); + return gen; + } + + if (Enum.class.isAssignableFrom(type)) { + // we have an enum: + return new EnumGen(type); + } + return null; + } + public void addStaticFactory(Class clazz, Function copyFunc) { staticFactories.add(new StaticFactory(clazz, copyFunc)); } @@ -145,4 +187,17 @@ public void addStaticFactory(Class clazz, Class> generator) { + Type superType = GenericTypeReflector.getExactSuperType(generator, SeriesGen.class); + if (superType instanceof ParameterizedType) { + Type gt = ((ParameterizedType) superType).getActualTypeArguments()[0]; + typeGeneratorClasses.put(gt, generator); + typeGenerators.remove(gt); + } else { + throw new RuntimeException("Generator " + generator + " must extend SeriesGen with a concrete type parameter, but it only extends " + superType); + } + } + } diff --git a/src/main/java/smallcheck/generators/RestartException.java b/src/main/java/smallcheck/generators/RestartException.java new file mode 100644 index 0000000..7de6bc5 --- /dev/null +++ b/src/main/java/smallcheck/generators/RestartException.java @@ -0,0 +1,18 @@ +package smallcheck.generators; + +/** + * + */ +public class RestartException extends RuntimeException { + private final int stackDepth; + + public RestartException(int stackDepth) { + this.stackDepth = stackDepth; + } + + public int getStackDepth() { + return stackDepth; + } + + +} diff --git a/src/main/java/smallcheck/generators/StateGen.java b/src/main/java/smallcheck/generators/StateGen.java new file mode 100644 index 0000000..090350f --- /dev/null +++ b/src/main/java/smallcheck/generators/StateGen.java @@ -0,0 +1,115 @@ +package smallcheck.generators; + +import java.lang.reflect.Type; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * A stateful generator + */ +public class StateGen { + private List stack = new ArrayList<>(); + private final GenFactory genFactory; + private int stackDepth = 0; + private final int depth; + private int replayUntil = 0; + + public StateGen(GenFactory genFactory, int depth) { + this.genFactory = genFactory; + this.depth = depth; + } + + public void restart(int replayUntil) { + this.replayUntil = replayUntil; + stackDepth = 0; + } + + public void restart() { + this.replayUntil = stackDepth - 1; + stackDepth = 0; + } + + public Object gen(Type t) { + return gen(() -> genFactory.genForType(t)); + } + + @SuppressWarnings("unchecked") + public T gen(Class t) { + return (T) gen(() -> genFactory.genForType(t)); + } + + @SuppressWarnings("unchecked") + public T gen(Supplier> genSupplier) { + if (stackDepth < stack.size()) { + IteratorWithCurrent gen = stack.get(stackDepth); + if (stackDepth < replayUntil) { + stackDepth++; + return (T) gen.getCurrent(); + } else if (gen.hasNext()) { + stackDepth++; + return (T) gen.next(); + } else { + clearStack(stackDepth); + throw new RestartException(stackDepth); + } + } else { + if (stackDepth < depth) { + IteratorWithCurrent it = new IteratorWithCurrent(genSupplier.get().generate(depth - stackDepth)); + stack.add(it); + if (it.hasNext()) { + stackDepth++; + return (T) it.next(); + } else { + throw new RestartException(stackDepth); + } + } else { + throw new RestartException(stackDepth); + } + } + } + + private void clearStack(int stackDepth) { + if (stack.size() > stackDepth) { + stack.subList(stackDepth, stack.size()).clear(); + } + } + + + + private static class IteratorWithCurrent implements Iterator { + private final Iterator it; + private Object current = null; + + public IteratorWithCurrent(Stream stream) { + it = stream.iterator(); + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public Object next() { + current = it.next(); + return current; + } + + public Object getCurrent() { + return current; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("StateGen:\n"); + for (IteratorWithCurrent i : stack) { + sb.append("\t\tgenerated ").append(i.getCurrent()).append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/java/smallcheck/generators/StringGen.java b/src/main/java/smallcheck/generators/StringGen.java index f6971b3..24cf920 100644 --- a/src/main/java/smallcheck/generators/StringGen.java +++ b/src/main/java/smallcheck/generators/StringGen.java @@ -7,16 +7,22 @@ */ public class StringGen extends SeriesGen { - private ArrayGen arrayGen = new ArrayGen<>(Character.class, new CharGen()); + private final ArrayGenG arrayGen; + + public StringGen() { + this(new CharGen()); + } + + public StringGen(ArrayGenG arrayGen) { + this.arrayGen = arrayGen; + } + + public StringGen(SeriesGen charGen) { + this.arrayGen = ArrayGenG.charArray(charGen); + } @Override public Stream generate(int depth) { - return arrayGen.generate(depth).map(ar -> { - char[] chars = new char[ar.length]; - for (int i = 0; i < chars.length; i++) { - chars[i] = ar[i]; - } - return new String(chars); - }); + return arrayGen.generate(depth).map(String::new); } } diff --git a/src/test/java/CustomGenerators.java b/src/test/java/CustomGenerators.java index c8dc0db..65cf428 100644 --- a/src/test/java/CustomGenerators.java +++ b/src/test/java/CustomGenerators.java @@ -4,6 +4,7 @@ import org.junit.runner.RunWith; import smallcheck.SmallCheckRunner; +import smallcheck.annotations.RegisterGenerator; import smallcheck.annotations.StaticFactory; import smallcheck.annotations.From; import smallcheck.annotations.Property; @@ -11,6 +12,8 @@ import smallcheck.generators.SeriesGen; import java.math.BigInteger; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -43,7 +46,7 @@ public void testExpr(Expr e) { public static class ExprFactory { - public static Number number(@From(CustomNumberGen.class) int i) { + public static Number number(int i) { return new Number(i); } @@ -110,7 +113,7 @@ public Mult(Expr left, Expr right) { @Override int evaluate() { - return left.evaluate() + right.evaluate(); + return left.evaluate() * right.evaluate(); } @Override @@ -123,7 +126,38 @@ public String toString() { public static class CustomNumberGen extends SeriesGen { @Override public Stream generate(int depth) { - return IntStream.range(0, 2+depth).map(i -> 2*i).boxed(); + return IntStream.range(0, 1 + depth).map(i -> 2 * i).boxed(); + } + } + + public static class CustomCharGen extends SeriesGen { + @Override + public Stream generate(int depth) { + return IntStream.range(0, 1+depth).mapToObj((int i) -> { + if (i % 2 == 0) { + return (char) ('a' + i / 2); + } else { + return (char) ('A' + i / 2); + } + }); } } + + @Property(maxDepth = 4) + @RegisterGenerator(CustomNumberGen.class) + public void genList(List list) { + System.out.println(list); + } + + @Property(maxDepth = 4) + public void genList2(List<@From(CustomNumberGen.class) Integer> list) { + System.out.println(list); + } + + @Property(maxDepth = 4) + @RegisterGenerator(CustomNumberGen.class) + @RegisterGenerator(CustomCharGen.class) + public void genMap(Map m) { + System.out.println(m); + } } diff --git a/src/test/java/CustomGenerators2.java b/src/test/java/CustomGenerators2.java index ada12a5..d86a7a4 100644 --- a/src/test/java/CustomGenerators2.java +++ b/src/test/java/CustomGenerators2.java @@ -131,7 +131,7 @@ public Mult(Expr left, Expr right) { @Override int evaluate() { - return left.evaluate() + right.evaluate(); + return left.evaluate() * right.evaluate(); } @Override diff --git a/src/test/java/LazyExample.java b/src/test/java/LazyExample.java new file mode 100644 index 0000000..7ac45b1 --- /dev/null +++ b/src/test/java/LazyExample.java @@ -0,0 +1,80 @@ +import org.junit.Assert; +import org.junit.Assume; +import org.junit.runner.RunWith; +import smallcheck.SmallCheckRunner; +import smallcheck.annotations.Property; +import smallcheck.generators.IntegerGen; +import smallcheck.generators.StateGen; + +import java.util.ArrayList; +import java.util.List; + +/** + * + */ +@RunWith(SmallCheckRunner.class) +public class LazyExample { + + static > boolean sorted(List list) { + for (int i = 0; i < list.size() - 1; i++) { + if (list.get(i).compareTo(list.get(i + 1)) > 0) { + return false; + } + } + return true; + } + + static > void insert(T elem, List list) { + for (int i = 0; i < list.size(); i++) { + if (elem.compareTo(list.get(i)) <= 0) { + list.add(i, elem); + return; + } + } + list.add(elem); + } + + @Property + public void test(StateGen g) { + int size = g.gen(Integer.class); + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + list.add(new LazyInt(g)); + } + Assume.assumeTrue(sorted(list)); + LazyInt elem = new LazyInt(g); + System.out.println("pre = " + list); + System.out.println("elem = " + elem); + insert(elem, list); + System.out.println("post = " + list); + Assert.assertTrue(sorted(list)); + } + + static class LazyInt implements Comparable { + private Integer i = null; + private StateGen g; + + public LazyInt(StateGen g) { + this.g = g; + } + + @Override + public int compareTo(LazyInt other) { + return Integer.compare(get(), other.get()); + } + + int get() { + if (i == null) { + i = g.gen(IntegerGen::new); + } + return i; + } + + @Override + public String toString() { + return i == null ? "?" : "" + i; + } + } + + +} diff --git a/src/test/java/Max3Example.java b/src/test/java/Max3Example.java new file mode 100644 index 0000000..8019f25 --- /dev/null +++ b/src/test/java/Max3Example.java @@ -0,0 +1,45 @@ +import org.junit.runner.RunWith; +import smallcheck.SmallCheckRunner; +import smallcheck.annotations.Property; +import smallcheck.generators.StateGen; + +import static org.junit.Assert.assertTrue; + +/** + * + */ +@RunWith(SmallCheckRunner.class) +public class Max3Example { + + static int max3(int x, int y, int z) { + if (x > y && x > z) { + return x; + } else if (y > x && y > z) { + return y; + } else { + return z; + } + } + + @Property + public void testMax3(int x, int y, int z) { + int result = max3(x, y, z); + assertTrue(result >= x); + assertTrue(result >= y); + assertTrue(result >= z); + } + + + @Property + public void testMax3S(StateGen sg) { + int x = sg.gen(Integer.class); + int y = sg.gen(Integer.class); + int z = sg.gen(Integer.class); + int result = max3(x, y, z); + assertTrue(result >= x); + assertTrue(result >= y); + assertTrue(result >= z); + } + + +} diff --git a/src/test/java/StandardTypes.java b/src/test/java/StandardTypes.java index 3c81b37..5ac8032 100644 --- a/src/test/java/StandardTypes.java +++ b/src/test/java/StandardTypes.java @@ -31,6 +31,17 @@ public void test(int x, long y) { } + @Property + public void generate(int x, char c) { + System.out.println(x + ", " + c); + } + + @Property(maxDepth = 6, maxInvocations = 1000, minExamples = 100, timeout = 30) + public void configuredExample(int x, char c) { + System.out.println(x + ", " + c); + } + + @Property public void testArray(Integer[] ar) { int sum = 0; diff --git a/src/test/java/StringCmpExample.java b/src/test/java/StringCmpExample.java new file mode 100644 index 0000000..23ea290 --- /dev/null +++ b/src/test/java/StringCmpExample.java @@ -0,0 +1,58 @@ +import org.junit.runner.RunWith; +import smallcheck.SmallCheckRunner; +import smallcheck.annotations.Property; +import smallcheck.generators.*; + +import java.util.Comparator; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +@RunWith(SmallCheckRunner.class) +public class StringCmpExample { + + + class StringLexicoIgnoreCaseComparator implements Comparator { + + @Override + public int compare(String first, String second) { + if ((first == null) || (second == null)) { + throw new NullPointerException(); + } + + if (first.equalsIgnoreCase(second)) { + return 0; + } + + return first.compareTo(second); + } + + } + + @Property(maxDepth = 3) + public void compareTransitive(StateGen gen) { + StringLexicoIgnoreCaseComparator cmp = new StringLexicoIgnoreCaseComparator(); + String x = gen.gen(() -> new StringGen(new CharGenCase())); + String y = gen.gen(() -> new StringGen(new CharGenCase())); + String z = gen.gen(() -> new StringGen(new CharGenCase())); + System.out.print("x = '" + x); + System.out.print("', y = '" + y); + System.out.println("', z = '" + z + "'"); + assumeTrue(cmp.compare(x, y) <= 0); + assumeTrue(cmp.compare(y, z) <= 0); + assertTrue(cmp.compare(x, z) <= 0); + } + + + @Property(maxDepth = 3) + public void genInts(StateGen gen) { + int x = gen.gen(IntegerGen::new); + int y = gen.gen(IntegerGen::new); + int z = gen.gen(IntegerGen::new); + System.out.print("x = '" + x); + System.out.print("', y = '" + y); + System.out.println("', z = '" + z + "'"); + } + + +}