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();
|
}
|
}
|