package au.id.zancanaro.javacheck.junit; import au.id.zancanaro.javacheck.Generator; import au.id.zancanaro.javacheck.ShrinkResult; import au.id.zancanaro.javacheck.ShrinkTree; import au.id.zancanaro.javacheck.annotations.Property; import au.id.zancanaro.javacheck.annotations.Seed; import au.id.zancanaro.javacheck.object.GeneratorProvider; import org.junit.AssumptionViolatedException; 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.Method; import java.util.*; @SuppressWarnings("WeakerAccess") public class Properties extends BlockJUnit4ClassRunner { private final GeneratorProvider provider; public Properties(Class classObject) throws InitializationError { super(classObject); provider = GeneratorProvider.DEFAULT_PROVIDER.withDataSources(classObject); } @Override protected void validateConstructor(List errors) { validateOnlyOneConstructor(errors); } @Override protected void validateTestMethods(List errors) { for (FrameworkMethod each : computeTestMethods()) { if (each.getAnnotation(Property.class) != null) { each.validatePublicVoid(false, errors); each.validateNoTypeParametersOnArgs(errors); } else { each.validatePublicVoidNoArg(false, errors); } } } @Override protected List computeTestMethods() { List testMethods = new ArrayList<>(super.computeTestMethods()); List theoryMethods = getTestClass().getAnnotatedMethods(Property.class); testMethods.removeAll(theoryMethods); testMethods.addAll(theoryMethods); return testMethods; } @Override public Statement methodBlock(final FrameworkMethod method) { return new GenerativeTester(method, getTestClass(), provider); } public static class GenerativeTester extends Statement { private final FrameworkMethod testMethod; private final TestClass testClass; private final GeneratorProvider provider; public GenerativeTester(FrameworkMethod testMethod, TestClass testClass, GeneratorProvider provider) { this.testMethod = testMethod; this.testClass = testClass; this.provider = provider; } private static 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 { Generator[] generators = Arrays.stream(method.getGenericParameterTypes()) .map(param -> provider.getGenerator(param, new Annotation[0], provider)) .toArray(Generator[]::new); Generator> 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); ShrinkTree> tree = generator.generate(random, size); try { runTest(tree.getValue().toArray()); assumptionsViolated = 0; } catch (AssumptionViolatedException ex) { numTests++; if (assumptionsViolated++ == 50) { throw new Error("Violated 50 assumptions in a row: failing test", ex); } } catch (Throwable ex) { throw new PropertyError(method.getName(), seed, shrink(tree, ex)); } } } } private ShrinkResult shrink(ShrinkTree> failed, Throwable originalEx) { // this array is a mutable container so the shutdown handler can see the new version ShrinkResult[] smallest = new ShrinkResult[]{ new ShrinkResult(failed.getValue(), originalEx, 0, 0)}; Thread shutdownHandler = makeShutdownHandler(smallest, originalEx); Runtime.getRuntime().addShutdownHook(shutdownHandler); int depth = 0; int visited = 0; Set> seenArgs = new HashSet<>(); Iterator>> trees = failed.getChildren().iterator(); while (trees.hasNext()) { ShrinkTree> tree = trees.next(); List value = tree.getValue(); boolean notSeen = seenArgs.add(Arrays.asList(value)); if (notSeen) { visited++; try { runTest(value.toArray()); } catch (AssumptionViolatedException ex) { // ignore, because it's not useful } catch (Throwable ex) { smallest[0] = new ShrinkResult(tree.getValue(), ex, depth, visited); Iterator>> children = tree.getChildren().iterator(); if (children.hasNext()) { depth++; trees = children; } else { break; } } } } Runtime.getRuntime().removeShutdownHook(shutdownHandler); return smallest[0]; } private Thread makeShutdownHandler(ShrinkResult[] smallest, Throwable originalException) { return new Thread(() -> { System.err.println("Signal received while shrinking.\n" + "Current best shrink is: " + smallest[0].args + "\n" + "Shrinking exception: " + smallest[0].thrown + "\n" + "Originally was: " + originalException); }); } public void runTest(final Object[] args) throws Throwable { new BlockJUnit4ClassRunner(testClass.getJavaClass()) { @Override protected void collectInitializationErrors(List errors) { // do nothing } @Override public Statement methodBlock(FrameworkMethod method) { return super.methodBlock(method); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { return new Statement() { @Override public void evaluate() throws Throwable { method.invokeExplosively(test, args); } }; } @Override public Object createTest() throws Exception { return getTestClass().getOnlyConstructor().newInstance(); } }.methodBlock(testMethod).evaluate(); } } }