package expression.parser; import base.*; import expression.ToMiniString; import expression.common.*; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; /** * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) */ public class ParserTestSet { private static final int D = 5; private static final List TEST_VALUES = new ArrayList<>(); static { Functional.addRange(TEST_VALUES, D, D); Functional.addRange(TEST_VALUES, D, -D); } public static final List CONSTS = List.of(0, 1, -1, 4, -4, 10, -10, 30, -30, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE); protected final ParserTester tester; protected final ParsedKind kind; private final boolean safe; protected final TestCounter counter; public ParserTestSet(final ParserTester tester, final ParsedKind kind) { this(tester, kind, true); } protected ParserTestSet(final ParserTester tester, final ParsedKind kind, final boolean safe) { this.tester = tester; this.kind = kind; this.safe = safe; counter = tester.getCounter(); } private void examples(final TestGenerator generator) { example(generator, "$x+2", (x, y, z) -> x + 2); example(generator, "2-$y", (x, y, z) -> 2 - y); example(generator, " 3* $z ", (x, y, z) -> 3 * z); example(generator, "$x/ - 2", (x, y, z) -> -x / 2); example(generator, "$x*$y+($z-1 )/10", (x, y, z) -> x * y + (int) (z - 1) / 10); example(generator, "-(-(-\t\t-5 + 16 *$x*$y) + 1 * $z) -(((-11)))", (x, y, z) -> -(-(5 + 16 * x * y) + z) + 11); example(generator, "" + Integer.MAX_VALUE, (x, y, z) -> (long) Integer.MAX_VALUE); example(generator, "" + Integer.MIN_VALUE, (x, y, z) -> (long) Integer.MIN_VALUE); example(generator, "$x--$y--$z", (x, y, z) -> x + y + z); example(generator, "((2+2))-0/(--2)*555", (x, y, z) -> 4L); example(generator, "$x-$x+$y-$y+$z-($z)", (x, y, z) -> 0L); example(generator, "(".repeat(300) + "$x + $y + (-10*-$z)" + ")".repeat(300), (x, y, z) -> x + y + 10 * z); example(generator, "$x / $y / $z", (x, y, z) -> y == 0 || z == 0 ? Reason.DBZ.error() : (int) x / (int) y / z); } private void example(final TestGenerator generator, final String expr, final ExampleExpression expression) { final List names = Functional.map(generator.variables(3), Pair::first); final TExpression expected = vars -> expression.evaluate(vars.get(0), vars.get(1), vars.get(2)); counter.test(() -> { final String mangled = mangle(expr, names); final E parsed = parse(mangled, names, true); Functional.allValues(TEST_VALUES, 3).forEach(values -> check(expected, parsed, names, values, mangled)); }); } protected static String mangle(final String expr, final List names) { return expr .replace("$x", names.get(0)) .replace("$y", names.get(1)) .replace("$z", names.get(2)); } protected void test() { final TestGenerator generator = tester.generator.build(kind.kind.variables()); final Renderer renderer = tester.renderer.build(); final Consumer> consumer = test -> test(renderer, test); counter.scope("Basic tests", () -> generator.testBasic(consumer)); counter.scope("Handmade tests", () -> examples(generator)); counter.scope("Random tests", () -> generator.testRandom(counter, 1, consumer)); } private void test(final Renderer renderer, final TestGenerator.Test test) { final Expr expr = test.expr; final List> vars = expr.variables(); final List variables = Functional.map(vars, Pair::first); final String full = test.render(NodeRenderer.FULL); final String mini = test.render(NodeRenderer.MINI); final E fullParsed = parse(test, variables, NodeRenderer.FULL); final E miniParsed = parse(test, variables, NodeRenderer.MINI); final E safeParsed = parse(test, variables, NodeRenderer.SAME); checkToString(full, mini, "base", fullParsed); if (tester.mode() > 0) { counter.test(() -> Asserts.assertEquals("mini.toMiniString", mini, miniParsed.toMiniString())); counter.test(() -> Asserts.assertEquals("safe.toMiniString", mini, safeParsed.toMiniString())); } checkToString(full, mini, "extraParentheses", parse(test, variables, NodeRenderer.FULL_EXTRA)); checkToString(full, mini, "noSpaces", parse(removeSpaces(full), variables, false)); checkToString(full, mini, "extraSpaces", parse(extraSpaces(full), variables, false)); final TExpression expected = renderer.render( Expr.of( expr.node(), Functional.map(vars, (i, var) -> Pair.of(var.first(), args -> args.get(i))) ), Unit.INSTANCE ); check(expected, fullParsed, variables, tester.random().random(variables.size(), ExtendedRandom::nextInt), full); if (this.safe) { final String safe = test.render(NodeRenderer.SAME); check(expected, safeParsed, variables, tester.random().random(variables.size(), ExtendedRandom::nextInt), safe); } } private E parse( final TestGenerator.Test test, final List variables, final NodeRenderer.Settings settings ) { return parse(test.render(settings.withParens(tester.parens)), variables, false); } private static final String LOOKBEHIND = "(?*/+=!-])"; private static final String LOOKAHEAD = "(?![a-zA-Z0-9<>*/])"; private static final Pattern SPACES = Pattern.compile(LOOKBEHIND + " | " + LOOKAHEAD + "|" + LOOKAHEAD + LOOKBEHIND); private String extraSpaces(final String expression) { return SPACES.matcher(expression).replaceAll(r -> tester.random().randomString( ExtendedRandom.SPACES, tester.random().nextInt(5) )); } private static String removeSpaces(final String expression) { return SPACES.matcher(expression).replaceAll(""); } private void checkToString(final String full, final String mini, final String context, final ToMiniString parsed) { counter.test(() -> { assertEquals(context + ".toString", full, full, parsed.toString()); if (tester.mode() > 0) { assertEquals(context + ".toMiniString", full, mini, parsed.toMiniString()); } }); } private static void assertEquals( final String context, final String original, final String expected, final String actual ) { final String message = String.format("%s:%n original `%s`,%n expected `%s`,%n actual `%s`", context, original, expected, actual); Asserts.assertTrue(message, Objects.equals(expected, actual)); } private Either eval(final TExpression expression, final List vars) { return Reason.eval(() -> tester.cast(expression.evaluate(vars))); } protected E parse(final String expression, final List variables, final boolean reparse) { return counter.testV(() -> { final E parsed = counter.testV(() -> counter.call("parse", () -> kind.parse(expression, variables))); if (reparse) { counter.testV(() -> counter.call("parse", () -> kind.parse(parsed.toString(), variables))); } return parsed; }); } private void check( final TExpression expectedExpression, final E expression, final List variables, final List values, final String unparsed ) { counter.test(() -> { final Either answer = eval(expectedExpression, values); final String args = IntStream.range(0, variables.size()) .mapToObj(i -> variables.get(i) + "=" + values.get(i)) .collect(Collectors.joining(", ")); final String message = String.format("f(%s)%n\twhere f=%s%n\tyour f=%s", args, unparsed, expression); try { final C actual = kind.kind.evaluate(expression, variables, kind.kind.fromInts(values)); counter.checkTrue(answer.isRight(), "Error expected for f(%s)%n\twhere f=%s%n\tyour f=%s", args, unparsed, expression); Asserts.assertEquals(message, answer.getRight(), actual); } catch (final Exception e) { if (answer.isRight()) { counter.fail(e, "No error expected for %s", message); } } }); } @FunctionalInterface public interface TExpression { long evaluate(List vars); } @FunctionalInterface protected interface ExampleExpression { long evaluate(long x, long y, long z); } /** * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) */ public record ParsedKind(ExpressionKind kind, Parser parser) { public E parse(final String expression, final List variables) throws Exception { return parser.parse(expression, variables); } @Override public String toString() { return kind.toString(); } } @FunctionalInterface public interface Parser { E parse(final String expression, final List variables) throws Exception; } }