diff --git a/java/expression/parser/ParserTestSet.java b/java/expression/parser/ParserTestSet.java new file mode 100644 index 0000000..7abcbfd --- /dev/null +++ b/java/expression/parser/ParserTestSet.java @@ -0,0 +1,398 @@ +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; + } +} diff --git a/java/expression/parser/ParserTester.java b/java/expression/parser/ParserTester.java new file mode 100644 index 0000000..16dfb0e --- /dev/null +++ b/java/expression/parser/ParserTester.java @@ -0,0 +1,112 @@ +package expression.parser; + +import base.ExtendedRandom; +import base.TestCounter; +import base.Tester; +import base.Unit; +import expression.ToMiniString; +import expression.common.*; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.LongBinaryOperator; +import java.util.function.LongToIntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ParserTester extends Tester { + + /* package-private */ final TestGeneratorBuilder generator; + /* package-private */ final Renderer.Builder< + Integer, + Unit, + ParserTestSet.TExpression + > renderer; + private final List> kinds = + new ArrayList<>(); + /* package-private */ final List parens = + new ArrayList<>(List.of(NodeRenderer.paren("(", ")"))); + + public ParserTester(final TestCounter counter) { + super(counter); + renderer = Renderer.builder(c -> vars -> c); + final ExtendedRandom random = counter.random(); + generator = new TestGeneratorBuilder<>( + random, + random::nextInt, + ParserTestSet.CONSTS, + true + ); + } + + public void unary( + final String name, + final int priority, + final BiFunction op + ) { + generator.unary(name, priority); + renderer.unary( + name, + (unit, a) -> vars -> cast(op.apply(a.evaluate(vars), this::cast)) + ); + } + + public void unary( + final String left, + final String right, + final BiFunction op + ) { + generator.unary(left, right); + renderer.unary( + left, + (unit, a) -> vars -> cast(op.apply(a.evaluate(vars), this::cast)) + ); + } + + public void binary( + final String name, + final int priority, + final LongBinaryOperator op + ) { + generator.binary(name, priority); + renderer.binary( + name, + (unit, a, b) -> + vars -> cast(op.applyAsLong(a.evaluate(vars), b.evaluate(vars))) + ); + } + + void kind( + final ExpressionKind kind, + final ParserTestSet.Parser parser + ) { + kinds.add(new ParserTestSet.ParsedKind<>(kind, parser)); + } + + @Override + public void test() { + for (final ParserTestSet.ParsedKind kind : kinds) { + counter.scope(kind.toString(), () -> test(kind)); + } + } + + protected void test(final ParserTestSet.ParsedKind kind) { + new ParserTestSet<>(this, kind).test(); + } + + public TestCounter getCounter() { + return counter; + } + + protected int cast(final long value) { + return (int) value; + } + + 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])); + } + } +}