package expression; import static base.Asserts.assertTrue; 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; /** * @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< ? extends E >) 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); }); } } }