package jnpf.elnFormula; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class FormulaParserTest { private final FormulaParser parser = new FormulaParser(); @Test public void parsesSupportedFormulaLanguage() { assertNode("12.50"); assertNode("\"result\\ntext\""); assertNode("true"); assertNode("false"); assertNode("null"); assertNode("sample_weight_a1"); assertNode("(sample_weight_a1 * factor_b2) / volume_c3"); assertNode("IF(IS_BLANK(observed_a1), fallback_b2, observed_a1)"); assertNode("!(result_a1 >= 1.5 && result_a1 < 10) || result_a1 == 0"); assertNode("-2 ^ -2 + 5 % 2"); } @Test public void buildsOwnImmutableAstWithSourceOffsets() { FormulaAst.Node node = parser.parse("sample_a1 + 2"); assertTrue(node instanceof FormulaAst.Binary); assertEquals(0, node.getStartOffset()); assertEquals(13, node.getEndOffset()); FormulaAst.Binary binary = (FormulaAst.Binary) node; assertEquals("+", binary.getOperator()); assertTrue(binary.getLeft() instanceof FormulaAst.Parameter); assertTrue(binary.getRight() instanceof FormulaAst.Literal); } @Test public void rejectsCapabilitiesOutsideControlledDsl() { assertRejected("java.lang.Runtime.getRuntime()", "LEXICAL_ERROR"); assertRejected("x[0]", "LEXICAL_ERROR"); assertRejected("x = 1", "LEXICAL_ERROR"); assertRejected("x; 1", "LEXICAL_ERROR"); assertRejected("new", "FORBIDDEN_IDENTIFIER"); assertRejected("method()", "SYNTAX_ERROR"); assertRejected("1 // comment", "SYNTAX_ERROR"); assertRejected("1 /* comment */", "SYNTAX_ERROR"); } @Test public void rejectsMalformedTokensAndSyntaxWithoutLeakingParserErrors() { assertRejected("\"unterminated", "LEXICAL_ERROR"); assertRejected("IF(1, 2", "SYNTAX_ERROR"); assertRejected("1 +", "SYNTAX_ERROR"); assertRejected("", "SYNTAX_ERROR"); } @Test public void enforcesExpressionIdentifierTokenAndDepthLimits() { assertRejected(repeat('a', 8193), "EXPRESSION_TOO_LONG"); assertRejected(repeat('a', 81), "IDENTIFIER_TOO_LONG"); assertRejected(repeat('A', 81) + "()", "IDENTIFIER_TOO_LONG"); StringBuilder tokens = new StringBuilder(); for (int i = 0; i < 2049; i++) { if (i > 0) { tokens.append('+'); } tokens.append('1'); } assertRejected(tokens.toString(), "TOKEN_LIMIT_EXCEEDED"); assertNode(nested(63)); assertRejected(nested(65), "AST_DEPTH_EXCEEDED"); } @Test public void exposesStableDiagnosticLocation() { try { parser.parse("sample_a1 + @"); fail("expected parse rejection"); } catch (FormulaParser.ParseException ex) { assertEquals("LEXICAL_ERROR", ex.getCode()); assertEquals(12, ex.getStartOffset()); assertEquals(13, ex.getEndOffset()); assertNotNull(ex.getMessage()); assertTrue(!ex.getMessage().contains("org.antlr")); } } @Test public void exposesUtf16OffsetsAfterSupplementaryCharacters() { FormulaAst.Binary logical = (FormulaAst.Binary) parser.parse( "\"😀\" == \"x\" && missing_a1"); FormulaAst.Parameter parameter = (FormulaAst.Parameter) logical.getRight(); assertEquals(15, parameter.getStartOffset()); assertEquals(25, parameter.getEndOffset()); try { parser.parse("\"😀\" == \"x\" &&"); fail("expected syntax rejection"); } catch (FormulaParser.ParseException ex) { assertEquals("SYNTAX_ERROR", ex.getCode()); assertEquals(14, ex.getStartOffset()); assertEquals(14, ex.getEndOffset()); } } private void assertNode(String expression) { assertTrue(parser.parse(expression) instanceof FormulaAst.Node); } private void assertRejected(String expression, String code) { try { parser.parse(expression); fail("expected " + code + " for: " + expression); } catch (FormulaParser.ParseException ex) { assertEquals(code, ex.getCode()); } } private static String nested(int depth) { StringBuilder value = new StringBuilder(depth * 2 + 1); for (int i = 0; i < depth; i++) { value.append('('); } value.append('1'); for (int i = 0; i < depth; i++) { value.append(')'); } return value.toString(); } private static String repeat(char value, int count) { StringBuilder result = new StringBuilder(count); for (int i = 0; i < count; i++) { result.append(value); } return result.toString(); } }