package expression; import base.*; import expression.common.*; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.*; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import static base.Asserts.assertTrue; /** * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) */ public class ExpressionTester extends Tester { private final List VALUES = IntStream.rangeClosed(-10, 10).boxed().toList(); private final ExpressionKind kind; private final List basic = new ArrayList<>(); private final List advanced = new ArrayList<>(); private final Set used = new HashSet<>(); private final GeneratorBuilder generator; private final List> prev = new ArrayList<>(); private final Map mappings; protected ExpressionTester( final TestCounter counter, final ExpressionKind kind, final Function expectedConstant, final Binary binary, final BinaryOperator add, final BinaryOperator sub, final BinaryOperator mul, final BinaryOperator div, final Map mappings ) { super(counter); this.kind = kind; this.mappings = mappings; generator = new GeneratorBuilder(expectedConstant, kind::constant, binary, kind::randomValue); generator.binary("+", 1600, add, Add.class); generator.binary("-", 1602, sub, Subtract.class); generator.binary("*", 2001, mul, Multiply.class); generator.binary("/", 2002, div, Divide.class); } protected ExpressionTester( final TestCounter counter, final ExpressionKind kind, final Function expectedConstant, final Binary binary, final BinaryOperator add, final BinaryOperator sub, final BinaryOperator mul, final BinaryOperator div ) { this(counter, kind, expectedConstant, binary, add, sub, mul, div, Map.of()); } @Override public String toString() { return kind.getName(); } @Override public void test() { counter.scope("Basic tests", () -> basic.forEach(Test::test)); counter.scope("Advanced tests", () -> advanced.forEach(Test::test)); counter.scope("Random tests", generator::testRandom); } @SuppressWarnings({"ConstantValue", "EqualsWithItself"}) private void checkEqualsAndToString(final String full, final String mini, final ToMiniString expression, final ToMiniString copy) { checkToString("toString", full, expression.toString()); if (mode() > 0) { checkToString("toMiniString", mini, expression.toMiniString()); } counter.test(() -> { assertTrue("Equals to this", expression.equals(expression)); assertTrue("Equals to copy", expression.equals(copy)); assertTrue("Equals to null", !expression.equals(null)); assertTrue("Copy equals to null", !copy.equals(null)); }); final String expressionToString = Objects.requireNonNull(expression.toString()); for (final Pair pair : prev) { counter.test(() -> { final ToMiniString prev = pair.first(); final String prevToString = pair.second(); final boolean equals = prevToString.equals(expressionToString); assertTrue("Equals to " + prevToString, prev.equals(expression) == equals); assertTrue("Equals to " + prevToString, expression.equals(prev) == equals); assertTrue("Inconsistent hashCode for " + prev + " and " + expression, (prev.hashCode() == expression.hashCode()) == equals); }); } } private void checkToString(final String method, final String expected, final String actual) { counter.test(() -> assertTrue(String.format("Invalid %s\n expected: %s\n actual: %s", method, expected, actual), expected.equals(actual))); } private void check( final String full, final E expected, final E actual, final List variables, final List values ) { final String vars = IntStream.range(0, variables.size()) .mapToObj(i -> variables.get(i) + "=" + values.get(i)) .collect(Collectors.joining(",")); counter.test(() -> { final Object expectedResult = evaluate(expected, variables, values); final Object actualResult = evaluate(actual, variables, values); final String reason = String.format( "%s:%n expected `%s`,%n actual `%s`", String.format("f(%s)\nwhere f is %s", vars, full), Asserts.toString(expectedResult), Asserts.toString(actualResult) ); if ( expectedResult != null && actualResult != null && expectedResult.getClass() == actualResult.getClass() && (expectedResult.getClass() == Double.class || expectedResult.getClass() == Float.class) ) { final double expectedValue = ((Number) expectedResult).doubleValue(); final double actualValue = ((Number) actualResult).doubleValue(); Asserts.assertEquals(reason, expectedValue, actualValue, 1e-6); } else { assertTrue(reason, Objects.deepEquals(expectedResult, actualResult)); } }); } private Object evaluate(final E expression, final List variables, final List values) { try { return kind.evaluate(expression, variables, values); } catch (final Exception e) { return e.getClass().getName(); } } protected ExpressionTester basic(final String full, final String mini, final E expected, final E actual) { return basicF(full, mini, expected, vars -> actual); } protected ExpressionTester basicF(final String full, final String mini, final E expected, final Function, E> actual) { return basic(new Test(full, mini, expected, actual)); } private ExpressionTester basic(final Test test) { Asserts.assertTrue(test.full, used.add(test.full)); basic.add(test); return this; } protected ExpressionTester advanced(final String full, final String mini, final E expected, final E actual) { return advancedF(full, mini, expected, vars -> actual); } protected ExpressionTester advancedF(final String full, final String mini, final E expected, final Function, E> actual) { Asserts.assertTrue(full, used.add(full)); advanced.add(new Test(full, mini, expected, actual)); return this; } protected static Named variable(final String name, final E expected) { return Named.of(name, expected); } @FunctionalInterface public interface Binary { E apply(BinaryOperator op, E a, E b); } private final class Test { private final String full; private final String mini; private final E expected; private final Function, E> actual; private Test(final String full, final String mini, final E expected, final Function, E> actual) { this.full = full; this.mini = mini; this.expected = expected; this.actual = actual; } private void test() { final List> variables = kind.variables().generate(random(), 3); final List names = Functional.map(variables, Pair::first); final E actual = kind.cast(this.actual.apply(names)); final String full = mangle(this.full, names); final String mini = mangle(this.mini, names); counter.test(() -> { kind.allValues(variables.size(), VALUES).forEach(values -> check(mini, expected, actual, names, values)); checkEqualsAndToString(full, mini, actual, actual); prev.add(Pair.of(actual, full)); }); } private String mangle(String string, final List names) { for (int i = 0; i < names.size(); i++) { string = string.replace("$" + (char) ('x' + i), names.get(i)); } for (final Map.Entry mapping : mappings.entrySet()) { string = string.replace(mapping.getKey(), mapping.getValue().toString()); } return string; } } private final class GeneratorBuilder { private final Generator.Builder generator; private final NodeRendererBuilder renderer = new NodeRendererBuilder<>(random()); private final Renderer.Builder expected; private final Renderer.Builder actual; private final Renderer.Builder copy; private final Binary binary; private GeneratorBuilder( final Function expectedConstant, final Function actualConstant, final Binary binary, final Function randomValue ) { generator = Generator.builder(() -> randomValue.apply(random()), random()); expected = Renderer.builder(expectedConstant::apply); actual = Renderer.builder(actualConstant::apply); copy = Renderer.builder(actualConstant::apply); this.binary = binary; } private void binary(final String name, final int priority, final BinaryOperator op, final Class type) { generator.add(name, 2); renderer.binary(name, priority); expected.binary(name, (unit, a, b) -> binary.apply(op, a, b)); @SuppressWarnings("unchecked") final Constructor constructor = (Constructor) Arrays.stream(type.getConstructors()) .filter(cons -> Modifier.isPublic(cons.getModifiers())) .filter(cons -> cons.getParameterCount() == 2) .findFirst() .orElseGet(() -> counter.fail("%s(..., ...) constructor not found", type.getSimpleName())); final Renderer.BinaryOperator actual = (unit, a, b) -> { try { return constructor.newInstance(a, b); } catch (final Exception e) { return counter.fail(e); } }; this.actual.binary(name, actual); copy.binary(name, actual); } private void testRandom() { final NodeRenderer renderer = this.renderer.build(); final Renderer expectedRenderer = this.expected.build(); final Renderer actualRenderer = this.actual.build(); final expression.common.Generator generator = this.generator.build(kind.variables(), List.of()); generator.testRandom(counter, 1, expr -> { final String full = renderer.render(expr, NodeRenderer.FULL); final String mini = renderer.render(expr, NodeRenderer.MINI); final E expected = expectedRenderer.render(expr, Unit.INSTANCE); final E actual = actualRenderer.render(expr, Unit.INSTANCE); final List> variables = expr.variables(); final List names = Functional.map(variables, Pair::first); final List values = Stream.generate(() -> kind.randomValue(random())) .limit(variables.size()) .toList(); checkEqualsAndToString(full, mini, actual, copy.build().render(expr, Unit.INSTANCE)); check(full, expected, actual, names, values); }); } } }