package expression.generic; import base.*; import expression.ToMiniString; import expression.common.*; import expression.parser.ParserTestSet; import java.util.*; import java.util.function.*; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; /** * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) */ public class GenericTester extends Tester { private static final int SIZE = 10; private static final int MAX = Integer.MAX_VALUE - 1; private static final int MIN = Integer.MIN_VALUE; private static final List>> VARIABLES = List.of( Pair.of("x", (x, y, z) -> x), Pair.of("y", (x, y, z) -> y), Pair.of("z", (x, y, z) -> z) ); private static final ExpressionKind.Variables> VARS = (c, r) -> VARIABLES; protected final List>> tests = new ArrayList<>(); private final Tabulator tabulator = new GenericTabulator(); private final Set operations = new HashSet<>(); private final List parens = new ArrayList<>( List.of(NodeRenderer.paren("(", ")")) ); private final TestGeneratorBuilder generator; private final List> modes = new ArrayList<>(); public GenericTester(final TestCounter counter) { super(counter); generator = new TestGeneratorBuilder<>( random(), random()::nextInt, ParserTestSet.CONSTS, false ); } protected void test( final String expression, final String name, final IF f ) { tests.add(Named.of(name + ": " + expression, f)); } @Override public void test() { final List modes = this.modes.stream() .map(mode -> mode.build(this)) .toList(); for (final Named> test : tests) { final String[] parts = test.name().split(": "); testFull(parts[0], parts[1], test.value()); } final TestGenerator> generator = this.generator.build( VARS ); counter.scope("basic", () -> generator.testBasic(test(true, modes))); counter.scope("random", () -> generator.testRandom( counter, 20 + (TestCounter.DENOMINATOR - 1) * 2, test(false, modes) ) ); } private void testFull( final String mode, final String expression, final IF f ) { testShort(mode, expression, f); test(mode, expression, f, MAX, -1, MAX, 0); test(mode, expression, f, MIN, 0, MIN, 1); } private void testShort( final String mode, final String expression, final IF f ) { test(mode, expression, f, 0, -1, 0, 1); } private void test( final String mode, final String expression, final IF f, final int min, final int dMin, final int max, final int dMax ) { test( mode, expression, f, min + random().nextInt(SIZE) * dMin, max + random().nextInt(SIZE) * dMax, min + random().nextInt(SIZE) * dMin, max + random().nextInt(SIZE) * dMax, min + random().nextInt(SIZE) * dMin, max + random().nextInt(SIZE) * dMax ); } private Consumer>> test( final boolean full, final List modes ) { final NodeRenderer.Settings settings = NodeRenderer.FULL.withParens( parens ); return test -> modes.forEach(mode -> mode.test(test.expr, test.render(settings), full) ); } private void test( final String mode, final String expression, final IF f, final int x1, final int x2, final int y1, final int y2, final int z1, final int z2 ) { final String context = String.format( "mode=%s, x=[%d, %d] y=[%d, %d] z=[%d, %d], expression=%s%n", mode, x1, x2, y1, y2, z1, z2, expression ); final Object[][][] result = counter.testV(() -> TestCounter.get(() -> tabulator.tabulate(mode, expression, x1, x2, y1, y2, z1, z2) ).either( e -> counter.fail(e, "%s %s", "tabulate", context), Function.identity() ) ); IntStream.rangeClosed(x1, x2).forEach(x -> IntStream.rangeClosed(y1, y2).forEach(y -> IntStream.rangeClosed(z1, z2).forEach(z -> counter.test(() -> { final Object expected = TestCounter.get(() -> f.apply(x, y, z) ).either(e -> null, Function.identity()); final Object actual = result[x - x1][y - y1][z - z1]; counter.checkTrue( Objects.equals(actual, expected), "table[%d][%d][%d](x=%d, y=%d, z=%d]) = %s (expected %s)%n%s", x - x1, y - y1, z - z1, x, y, z, actual, expected, context ); }) ) ) ); } public void binary(final String name, final int priority) { operations.add(name + ":2"); generator.binary(name, priority); } public void unary(final String name, final int priority) { operations.add(name + ":1"); generator.unary(name, priority); } public void parens(final String... parens) { assert parens.length % 2 == 0 : "Parens should come in pairs"; for (int i = 0; i < parens.length; i += 2) { this.parens.add(NodeRenderer.paren(parens[i], parens[i + 1])); } } /* package-private */ interface Mode { static Builder builder( final String mode, final IntFunction constant, final IntUnaryOperator fixer ) { return new Builder<>(mode, constant, fixer); } void test( final Expr> expr, final String expression, final boolean full ); /* package-private */ final class Builder< T > implements Consumer { private final String mode; private final IntFunction constant; private final IntUnaryOperator fixer; private final List>>> unary = new ArrayList<>(); private final List< Named>> > binary = new ArrayList<>(); private Builder( final String mode, final IntFunction constant, final IntUnaryOperator fixer ) { this.mode = mode; this.constant = constant; this.fixer = fixer; } public Builder unary( final String name, final UnaryOperator op ) { unary.add( Named.of( name, arg -> (x, y, z) -> op.apply(arg.apply(x, y, z)) ) ); return this; } public Builder binary( final String name, final BinaryOperator op ) { binary.add( Named.of( name, (a, b) -> (x, y, z) -> op.apply(a.apply(x, y, z), b.apply(x, y, z)) ) ); return this; } @Override public void accept(final GenericTester tester) { tester.modes.add(this); } private Mode build(final GenericTester tester) { final Set ops = Stream.concat( unary.stream().map(op -> op.name() + ":1"), binary.stream().map(op -> op.name() + ":2") ).collect(Collectors.toUnmodifiableSet()); final List diff = tester.operations .stream() .filter(Predicate.not(ops::contains)) .toList(); Asserts.assertTrue( String.format("Missing operations for %s: %s", mode, diff), diff.isEmpty() ); final Renderer.Builder< Integer, Unit, GenericTester.F > builder = Renderer.builder( value -> (x, y, z) -> constant.apply(value) ); unary.forEach(op -> builder.unary(op.name(), (unit, arg) -> op.value().apply(arg) ) ); binary.forEach(op -> builder.binary(op.name(), (unit, a, b) -> op.value().apply(a, b) ) ); final Renderer> renderer = builder.build(); final TestGenerator> genRenderer = tester.generator.build(VARS); return (expr, expression, full) -> { final String fixed = fixer == IntUnaryOperator.identity() ? expression : genRenderer.render(cata(expr), NodeRenderer.FULL); @SuppressWarnings("unchecked") final Expr> converted = (Expr< Integer, F >) expr; final F expected = renderer.render( converted, Unit.INSTANCE ); final IF f = (x, y, z) -> expected.apply( constant.apply(x), constant.apply(y), constant.apply(z) ); if (full) { tester.testFull(mode, fixed, f); } else { tester.testShort(mode, fixed, f); } }; } private Expr> cata( final Expr> expr ) { return expr.node(node -> node.cata( c -> Node.constant(fixer.applyAsInt(c)), Node::op, Node::op, Node::op ) ); } } } @FunctionalInterface protected interface IF { T apply(int x, int y, int z); } @FunctionalInterface protected interface F extends ToMiniString { T apply(T x, T y, T z); } }