summaryrefslogtreecommitdiff
path: root/src/main/java/au/id/zancanaro/PropertyTestRunner.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/au/id/zancanaro/PropertyTestRunner.java')
-rw-r--r--src/main/java/au/id/zancanaro/PropertyTestRunner.java153
1 files changed, 153 insertions, 0 deletions
diff --git a/src/main/java/au/id/zancanaro/PropertyTestRunner.java b/src/main/java/au/id/zancanaro/PropertyTestRunner.java
new file mode 100644
index 0000000..fc90b8e
--- /dev/null
+++ b/src/main/java/au/id/zancanaro/PropertyTestRunner.java
@@ -0,0 +1,153 @@
+package au.id.zancanaro;
+
+import au.id.zancanaro.annotations.Property;
+import au.id.zancanaro.annotations.Seed;
+import junit.framework.AssertionFailedError;
+import org.junit.AssumptionViolatedException;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+public class PropertyTestRunner extends Runner {
+ private final Class<?> classUnderTest;
+
+ public PropertyTestRunner(Class<?> classUnderTest) {
+ this.classUnderTest = classUnderTest;
+ }
+
+ @Override
+ public Description getDescription() {
+ return Description.createSuiteDescription(classUnderTest);
+ }
+
+ private long getSeed(Method method) {
+ Seed seed = method.getAnnotation(Seed.class);
+ if (seed == null) {
+ return System.currentTimeMillis();
+ } else {
+ return seed.value();
+ }
+ }
+
+ @Override
+ public void run(RunNotifier notifier) {
+ Method[] methods = classUnderTest.getMethods();
+ for (Method method : methods) {
+ Property details = method.getAnnotation(Property.class);
+ if (details != null) {
+ Description description = Description.createTestDescription(classUnderTest, method.getName());
+ boolean failed = false;
+ int assumptionsFailed = 0;
+
+ long seed = getSeed(method);
+ 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;
+ }
+ RoseTree<Object[]> generated = generateArgs(random, size,
+ method.getGenericParameterTypes(),
+ method.getParameterAnnotations());
+ try {
+ method.invoke(obj, generated.getValue());
+ } catch (InvocationTargetException ex) {
+ if (ex.getTargetException() instanceof AssumptionViolatedException) {
+ assumptionsFailed++;
+ } else {
+ notifier.fireTestFailure(new Failure(description, ex.getTargetException()));
+ System.out.println("Test failed with seed: " + seed);
+ System.out.println("Failing arguments: " + Arrays.asList(generated.getValue()));
+ Object[] shrinkResult = shrink(method, 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));
+ }
+ failed = true;
+ }
+ } catch (IllegalAccessException ex) {
+ notifier.fireTestFailure(new Failure(description, ex));
+ failed = true;
+ }
+ }
+
+ if (assumptionsFailed > 0) {
+ System.out.println("Failed " + assumptionsFailed + " assumptions");
+ }
+ notifier.fireTestFinished(description);
+ }
+ }
+ }
+
+ 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 (Throwable ex) {
+ Iterator<RoseTree<Object[]>> children = tree.getChildren();
+ if (children.hasNext()) {
+ trees = children;
+ }
+ smallest = tree.getValue();
+ }
+ }
+ 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();
+ }
+
+
+ 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]);
+// }
+ }
+ return Generators.arrayGenerator(generators).generate(random, size);
+ }
+
+ private Generator<?> getGeneratorFromType(Type type) {
+ if (type instanceof Class) {
+ Class<?> clazz = (Class<?>) type;
+ if (clazz.isPrimitive() && clazz == Integer.TYPE) {
+ return Generators.integerGenerator();
+ } 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)");
+ }
+ }
+}