summaryrefslogtreecommitdiff
path: root/src/main/java/au
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/au')
-rw-r--r--src/main/java/au/id/zancanaro/PropertyTestError.java45
-rw-r--r--src/main/java/au/id/zancanaro/PropertyTestRunner.java323
-rw-r--r--src/main/java/au/id/zancanaro/ShrinkResult.java11
-rw-r--r--src/main/java/au/id/zancanaro/annotations/Property.java2
4 files changed, 252 insertions, 129 deletions
diff --git a/src/main/java/au/id/zancanaro/PropertyTestError.java b/src/main/java/au/id/zancanaro/PropertyTestError.java
new file mode 100644
index 0000000..afd614b
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/PropertyTestError.java
@@ -0,0 +1,45 @@
+package au.id.zancanaro;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+
+public class PropertyTestError extends AssertionError {
+ public PropertyTestError(String methodName, long seed, ShrinkResult shrunk) {
+ super(String.format("%s(%s)\n\tSeed: %s\n%s",
+ methodName, join(", ", shrunk.args),
+ seed,
+ shrunk.thrown.getMessage()));
+ initCause(shrunk.thrown);
+ }
+
+
+ public static String join(String delimiter, Object... params) {
+ return join(delimiter, Arrays.asList(params));
+ }
+
+ public static String join(String delimiter, Collection<Object> values) {
+ StringBuilder sb = new StringBuilder();
+ Iterator<Object> iter = values.iterator();
+ while (iter.hasNext()) {
+ Object next = iter.next();
+ sb.append(stringValueOf(next));
+ if (iter.hasNext()) {
+ sb.append(delimiter);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static String stringValueOf(Object next) {
+ if (next instanceof String) {
+ return '"' + ((String) next).replace("\"", "\\\"") + '"';
+ } else {
+ try {
+ return String.valueOf(next);
+ } catch (Throwable e) {
+ return "[toString failed]";
+ }
+ }
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/PropertyTestRunner.java b/src/main/java/au/id/zancanaro/PropertyTestRunner.java
index 8df9b2a..b3563ff 100644
--- a/src/main/java/au/id/zancanaro/PropertyTestRunner.java
+++ b/src/main/java/au/id/zancanaro/PropertyTestRunner.java
@@ -3,127 +3,204 @@ package au.id.zancanaro;
import au.id.zancanaro.annotations.Property;
import au.id.zancanaro.annotations.Seed;
import org.junit.AssumptionViolatedException;
-import org.junit.Ignore;
-import org.junit.runner.Description;
-import org.junit.runner.notification.Failure;
-import org.junit.runner.notification.RunNotifier;
-import org.junit.runners.ParentRunner;
+import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Type;
+import java.lang.reflect.*;
import java.util.*;
-public class PropertyTestRunner extends ParentRunner<FrameworkMethod> {
- private final Class<?> classUnderTest;
+public class PropertyTestRunner extends BlockJUnit4ClassRunner {
+ private final Map<Type, Generator<Object>> generators = new HashMap<>();
- public PropertyTestRunner(Class<?> classUnderTest) throws InitializationError{
- super(classUnderTest);
- this.classUnderTest = classUnderTest;
+ public PropertyTestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
}
@Override
- protected boolean isIgnored(FrameworkMethod child) {
- return child.getAnnotation(Ignore.class) != null;
+ protected void collectInitializationErrors(List<Throwable> errors) {
+ super.collectInitializationErrors(errors);
+ validateGeneratorFields(errors);
}
- @Override
- protected List<FrameworkMethod> getChildren() {
- List<FrameworkMethod> result = new ArrayList<>();
- for (Method method : classUnderTest.getDeclaredMethods()) {
- if (method.isAnnotationPresent(Property.class)
- && !method.isAnnotationPresent(Ignore.class)) {
- result.add(new FrameworkMethod(method));
+ private void validateGeneratorFields(List<Throwable> errors) {
+ Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
+
+ for (Field field : fields) {
+ Type type = field.getGenericType();
+ if (!(type instanceof ParameterizedType)) {
+ continue;
+ }
+ ParameterizedType ptype = (ParameterizedType) type;
+ if (!(ptype.getRawType() instanceof Class)) {
+ continue;
+ }
+ Class<?> c = (Class) ptype.getRawType();
+ if (c != Generator.class) {
+ continue;
+ }
+ if (!Modifier.isStatic(field.getModifiers())) {
+ errors.add(new Error("Generator field " + field.getName() + " must be static"));
+ }
+ if (!Modifier.isPublic(field.getModifiers())) {
+ errors.add(new Error("Generator field " + field.getName() + " must be public"));
}
}
- return result;
}
- @Override
- protected Description describeChild(FrameworkMethod child) {
- return Description.createTestDescription(classUnderTest, child.getName());
+ 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);
}
- @Override
- protected void runChild(FrameworkMethod child, RunNotifier notifier) {
- try {
- Property details = child.getAnnotation(Property.class);
- Description description = Description.createTestDescription(classUnderTest, child.getName());
- boolean failed = false;
- int assumptionsFailed = 0;
-
- long seed = getSeed(child.getMethod());
- Random random = new Random(seed);
- int numRuns = details.runs();
- for (int i = 0; i < numRuns && !failed; ++i) {
- int size = details.size();
- notifier.fireTestStarted(description);
- Object obj;
- try {
- obj = classUnderTest.getConstructor().newInstance();
- } catch (Throwable ex) { // HACKY
- System.out.println(ex);
- return;
+ private Map<Type, Generator<Object>> computeGenerators() {
+ if (generators.isEmpty()) {
+ Field[] fields = getTestClass().getJavaClass().getDeclaredFields();
+
+ for (Field field : fields) {
+ Type type = field.getGenericType();
+ if (!(type instanceof ParameterizedType)) {
+ continue;
+ }
+ ParameterizedType ptype = (ParameterizedType) type;
+ if (!(ptype.getRawType() instanceof Class)) {
+ continue;
+ }
+ Class<?> c = (Class) ptype.getRawType();
+ if (c != Generator.class) {
+ continue;
}
- RoseTree<Object[]> generated = generateArgs(random, size,
- child.getMethod().getGenericParameterTypes(),
- child.getMethod().getParameterAnnotations());
try {
- child.getMethod().invoke(obj, generated.getValue());
- } catch (InvocationTargetException ex) {
- if (ex.getTargetException() instanceof AssumptionViolatedException) {
- assumptionsFailed++;
- i--;
- } else {
- System.out.println("Test failed with seed: " + seed);
- System.out.println("Failing arguments: " + Arrays.asList(generated.getValue()));
- Object[] shrinkResult = shrink(child.getMethod(), obj, generated);
- if (shrinkResult == null) {
- System.out.println("Arguments could not be shrunk any further");
- } else {
- System.out.println("Arguments shrunk to: " + Arrays.asList(shrinkResult));
- }
- notifier.fireTestFailure(new Failure(description, ex.getTargetException()));
- failed = true;
+ Type target = ptype.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) {
- notifier.fireTestFailure(new Failure(description, ex));
- failed = true;
+
}
}
+ }
+ return generators;
+ }
- if (assumptionsFailed > 0) {
- System.out.println("Failed " + assumptionsFailed + " assumptions");
+ @Override
+ protected void validateConstructor(List<Throwable> errors) {
+ validateOnlyOneConstructor(errors);
+ }
+
+ @Override
+ protected void validateTestMethods(List<Throwable> errors) {
+ for (FrameworkMethod each : computeTestMethods()) {
+ if (each.getAnnotation(Property.class) != null) {
+ each.validatePublicVoid(false, errors);
+ each.validateNoTypeParametersOnArgs(errors);
+ } else {
+ each.validatePublicVoidNoArg(false, errors);
}
- notifier.fireTestFinished(description);
- } catch (Throwable ex) {
- ex.printStackTrace();
}
}
- private long getSeed(Method method) {
- Seed seed = method.getAnnotation(Seed.class);
- if (seed == null) {
- return System.currentTimeMillis();
- } else {
- return seed.value();
- }
+ @Override
+ protected List<FrameworkMethod> computeTestMethods() {
+ List<FrameworkMethod> testMethods = new ArrayList<>(super.computeTestMethods());
+ List<FrameworkMethod> theoryMethods = getTestClass().getAnnotatedMethods(Property.class);
+ testMethods.removeAll(theoryMethods);
+ testMethods.addAll(theoryMethods);
+ return testMethods;
}
- private Object[] shrink(Method method, Object obj, RoseTree<Object[]> failed) {
- Object[] smallest = failed.getValue();
- Iterator<RoseTree<Object[]>> trees = failed.getChildren();
- while (trees.hasNext()) {
- RoseTree<Object[]> tree = trees.next();
- try {
- method.invoke(obj, tree.getValue());
- } catch (InvocationTargetException ex) {
- if (!(ex.getTargetException() instanceof AssumptionViolatedException)) {
- smallest = tree.getValue();
+ @Override
+ public Statement methodBlock(final FrameworkMethod method) {
+ return new GenerativeTester(method, getTestClass(), computeGenerators());
+ }
+
+ public static class GenerativeTester extends Statement {
+ private final FrameworkMethod testMethod;
+ private final TestClass testClass;
+ private final Map<Type, Generator<Object>> generators;
+
+ public GenerativeTester(FrameworkMethod testMethod, TestClass testClass, Map<Type, Generator<Object>> generators) {
+ this.testMethod = testMethod;
+ this.testClass = testClass;
+ this.generators = generators;
+ }
+
+ private long getSeed(Method method) {
+ Seed seed = method.getAnnotation(Seed.class);
+ if (seed == null) {
+ return System.currentTimeMillis();
+ } else {
+ return seed.value();
+ }
+ }
+
+ @Override
+ public void evaluate() throws Throwable {
+ Method method = testMethod.getMethod();
+ if (method.getParameterCount() == 0) {
+ runTest(new Object[0]);
+ } else {
+ @SuppressWarnings("unchecked")
+ Generator<Object>[] generators = (Generator<Object>[]) new Generator[method.getParameterCount()];
+ int index = 0;
+ for (Type type : method.getGenericParameterTypes()) {
+ generators[index++] = this.generators.get(type);
+ }
+ Generator<Object[]> generator = Generator.tuple(generators);
+
+ long seed = getSeed(method);
+ Random random = new Random(seed);
+
+ Property property = testMethod.getAnnotation(Property.class);
+ int assumptionsViolated = 0;
+ int maxSize = property.maxSize();
+ int numTests = property.runs();
+ for (int i = 0; i < numTests; ++i) {
+ int size = Math.min(i + 1, maxSize);
+ RoseTree<Object[]> tree = generator.generate(random, size);
+ try {
+ runTest(tree.getValue());
+ assumptionsViolated = 0;
+ } catch (AssumptionViolatedException ex) {
+ i--;
+ if (assumptionsViolated++ == 50) {
+ throw new Error("Violated 50 assumptions in a row: failing test");
+ }
+ ;
+ } catch (Throwable ex) {
+ throw new PropertyTestError(method.getName(), seed, shrink(tree, ex));
+ }
+ }
+ }
+ }
+
+ private ShrinkResult shrink(RoseTree<Object[]> failed, Throwable originalEx) {
+ ShrinkResult smallest = new ShrinkResult(failed.getValue(), originalEx);
+ Iterator<RoseTree<Object[]>> trees = failed.getChildren();
+ while (trees.hasNext()) {
+ RoseTree<Object[]> tree = trees.next();
+ try {
+ runTest(tree.getValue());
+ } catch (AssumptionViolatedException ex) {
+ // ignore, because it's not useful
+ } catch (Throwable ex) {
+ smallest = new ShrinkResult(tree.getValue(), ex);
Iterator<RoseTree<Object[]>> children = tree.getChildren();
if (children.hasNext()) {
trees = children;
@@ -131,49 +208,39 @@ public class PropertyTestRunner extends ParentRunner<FrameworkMethod> {
break;
}
}
- } catch (IllegalAccessException ex) {
- System.out.println(ex);
}
+ return smallest;
}
- return smallest;
- }
- private <T> String printShrinkTree(RoseTree<T[]> generated) {
- StringBuilder builder = new StringBuilder();
- builder.append('(');
- builder.append(Arrays.toString(generated.getValue()));
- generated.getChildren().forEachRemaining((child) -> {
- builder.append(' ');
- builder.append(printShrinkTree(child));
- });
- builder.append(')');
- return builder.toString();
- }
+ public void runTest(final Object[] args) throws Throwable {
+ new BlockJUnit4ClassRunner(testClass.getJavaClass()) {
+ @Override
+ protected void collectInitializationErrors(
+ List<Throwable> errors) {
+ // do nothing
+ }
+ @Override
+ public Statement methodBlock(FrameworkMethod method) {
+ return super.methodBlock(method);
+ }
- private RoseTree<Object[]> generateArgs(Random random, int size, Type[] types, Annotation[][] annotations) {
- Generator<?>[] generators = new Generator[types.length];
- for (int i = 0; i < types.length; ++i) {
-// generators[i] = getGeneratorFromAnnotations(annotations[i]);
-// if (generators[i] == null) {
- generators[i] = getGeneratorFromType(types[i]);
-// }
- }
- @SuppressWarnings("unchecked")
- Generator<Object>[] argsGenerators = (Generator<Object>[]) generators;
- return Generator.tuple(argsGenerators).generate(random, size);
- }
+ @Override
+ protected Statement methodInvoker(FrameworkMethod method, Object test) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ method.invokeExplosively(test, args);
+ }
+ };
+ }
- private Generator<?> getGeneratorFromType(Type type) {
- if (type instanceof Class) {
- Class<?> clazz = (Class<?>) type;
- if (clazz.isPrimitive() && clazz == Integer.TYPE) {
- return Generators.integer();
- } else {
- throw new RuntimeException("Unknown type for generator (atm only int is supported)");
- }
- } else {
- throw new RuntimeException("Unknown type for generator (atm only int is supported)");
+ @Override
+ public Object createTest() throws Exception {
+ return getTestClass().getOnlyConstructor().newInstance();
+ }
+ }.methodBlock(testMethod).evaluate();
}
}
-}
+
+} \ No newline at end of file
diff --git a/src/main/java/au/id/zancanaro/ShrinkResult.java b/src/main/java/au/id/zancanaro/ShrinkResult.java
new file mode 100644
index 0000000..fb2f2d4
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/ShrinkResult.java
@@ -0,0 +1,11 @@
+package au.id.zancanaro;
+
+class ShrinkResult {
+ public final Object[] args;
+ public final Throwable thrown;
+
+ public ShrinkResult(Object[] args, Throwable thrown) {
+ this.args = args;
+ this.thrown = thrown;
+ }
+}
diff --git a/src/main/java/au/id/zancanaro/annotations/Property.java b/src/main/java/au/id/zancanaro/annotations/Property.java
index cfe8e45..f750596 100644
--- a/src/main/java/au/id/zancanaro/annotations/Property.java
+++ b/src/main/java/au/id/zancanaro/annotations/Property.java
@@ -8,6 +8,6 @@ import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
- int size() default 100;
+ int maxSize() default 100;
int runs() default 100;
}