package au.id.zancanaro.javacheck; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.IntStream; import java.util.stream.Stream; @SuppressWarnings("unused") public class ShrinkTree { private final T value; private final Supplier>> children; private ShrinkTree(T value, Supplier>> children) { this.value = value; this.children = children; } public T getValue() { return value; } public Stream> getChildren() { return children.get(); } @SuppressWarnings("Convert2MethodRef") public static ShrinkTree pure(T value) { // Converting the () -> Stream.empty() into Stream::empty actually // breaks Java's generic type inference. Who would have thought? return new ShrinkTree<>(value, () -> Stream.empty()); } public static ShrinkTree join(ShrinkTree> tree) { return new ShrinkTree<>( tree.getValue().getValue(), () -> Stream.concat( tree.getChildren().map(ShrinkTree::join), tree.getValue().getChildren())); } private static List makeHeadList(ShrinkTree[] trees) { List heads = new ArrayList<>(trees.length); for (ShrinkTree tree : trees) { heads.add(tree.getValue()); } return heads; } @SuppressWarnings("unchecked") public static Stream[]> promoteChildren(ShrinkTree[] trees) { return IntStream.range(0, trees.length) .mapToObj(index -> trees[index].getChildren().map(child -> IntStream.range(0, trees.length) .mapToObj(i -> (i == index ? child : trees[i])) .toArray(ShrinkTree[]::new))) .flatMap(x -> x) .map(x -> (ShrinkTree[]) x); } public static Stream[]> removeChildren(ShrinkTree[] trees) { return IntStream.range(0, trees.length) .mapToObj(index -> IntStream.range(0, trees.length) .filter(i -> i != index) .mapToObj(i -> trees[i]) .toArray(ShrinkTree[]::new)); } public static Stream[]> removeAndPromoteChildren(ShrinkTree[] trees) { return Stream.concat(removeChildren(trees), promoteChildren(trees)); } public static ShrinkTree> combine( ShrinkTree[] trees, Function[], Stream[]>> processChildren) { return new ShrinkTree<>( makeHeadList(trees), () -> processChildren.apply(trees) .map(shrinks -> combine(shrinks, processChildren))); } public ShrinkTree map(Function f) { return new ShrinkTree<>( f.apply(this.value), () -> this.getChildren().map(tree -> tree.map(f))); } public ShrinkTree flatMap(Function> f) { return ShrinkTree.join(this.map(f)); } public ShrinkTree filter(Predicate predicate) { if (predicate.test(this.getValue())) { return new ShrinkTree<>( this.getValue(), () -> this.getChildren() .filter(tree -> predicate.test(tree.getValue())) .map(tree -> tree.filter(predicate))); } else { throw new IllegalArgumentException("Current value doesn't match predicate: whoops!"); } } public ShrinkTree withShrinkStrategy(ShrinkStrategy strategy) { return new ShrinkTree<>(this.getValue(), () -> strategyStream(this.getValue(), strategy)); } private static Stream> strategyStream(final T value, final ShrinkStrategy strategy) { return strategy.shrink(value) .map(v -> new ShrinkTree(v, () -> strategyStream(v, strategy))); } public void print(Writer output) throws IOException { print(output, Object::toString); } public void print(Writer output, Function toString) throws IOException { output.write(toString.apply(this.getValue())); output.write('['); Iterator> iterator = children.get().iterator(); while (iterator.hasNext()) { iterator.next().print(output, toString); if (iterator.hasNext()) { output.write(' '); } } output.write(']'); output.flush(); } }