package jnpf.elnService.formula;
|
|
import jnpf.elnFormula.FormulaAst;
|
import jnpf.elnFormula.FormulaDiagnostic;
|
import jnpf.elnFormula.FormulaLatexRenderer;
|
import jnpf.elnFormula.FormulaType;
|
import jnpf.elnService.formula.FormulaValidationService.FormulaValidationRequest;
|
import jnpf.elnService.formula.FormulaValidationService.ParameterSlot;
|
import jnpf.elnService.formula.FormulaValidationService.ValidationResult;
|
import jnpf.elnService.formula.impl.FormulaValidationServiceImpl;
|
import org.junit.Test;
|
|
import java.math.BigDecimal;
|
import java.util.Arrays;
|
import java.util.Collections;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertTrue;
|
|
public class FormulaValidationServiceTest {
|
|
private final FormulaValidationService service = new FormulaValidationServiceImpl();
|
|
@Test
|
public void reportsUnknownParameterWithExactParameterOffsets() {
|
ValidationResult result = validate("1 + missing_a1", slots(), FormulaType.DECIMAL);
|
|
assertError(result, "UNKNOWN_PARAMETER", 4, 14);
|
assertNull(result.getLatex());
|
}
|
|
@Test
|
public void reportsUnknownParameterWithUtf16OffsetsAfterEmoji() {
|
ValidationResult result = validate("\"😀\" == \"x\" && missing_a1",
|
slots(), FormulaType.BOOLEAN);
|
|
assertError(result, "UNKNOWN_PARAMETER", 15, 25);
|
}
|
|
@Test
|
public void reportsUnusedParameterAsWarningAndStillRendersLatex() {
|
ValidationResult result = validate("sample_a1 / 2",
|
slots(slot("sample_a1", "样品质量", FormulaType.DECIMAL),
|
slot("unused_a1", "未使用", FormulaType.TEXT)), FormulaType.DECIMAL);
|
|
assertFalse(result.hasErrors());
|
assertDiagnostic(result, FormulaDiagnostic.Severity.WARNING,
|
"UNUSED_PARAMETER", -1, -1);
|
assertEquals("\\frac{\\text{样品质量}}{2}", result.getLatex());
|
}
|
|
@Test
|
public void rejectsParameterAndFunctionCodeCollisionCaseInsensitively() {
|
ValidationResult result = validate("1", slots(slot("abs", "绝对值参数", FormulaType.DECIMAL)),
|
FormulaType.DECIMAL);
|
|
assertError(result, "PARAMETER_FUNCTION_NAME_CONFLICT", -1, -1);
|
assertNull(result.getLatex());
|
}
|
|
@Test
|
public void reportsFunctionArgumentCountWithExactCallOffsets() {
|
ValidationResult result = validate("ABS(1, 2)", slots(), FormulaType.DECIMAL);
|
|
assertError(result, "FUNCTION_ARGUMENT_COUNT_MISMATCH", 0, 9);
|
}
|
|
@Test
|
public void reportsBinaryOperandTypeWithExactExpressionOffsets() {
|
ValidationResult result = validate("text_a1 + 1",
|
slots(slot("text_a1", "文本", FormulaType.TEXT)), FormulaType.DECIMAL);
|
|
assertError(result, "TYPE_MISMATCH", 0, 11);
|
}
|
|
@Test
|
public void rejectsMismatchedDeclaredResultType() {
|
ValidationResult result = validate("1 > 0", slots(), FormulaType.DECIMAL);
|
|
assertError(result, "RESULT_TYPE_MISMATCH", 0, 5);
|
assertEquals(FormulaType.BOOLEAN, result.getInferredType());
|
}
|
|
@Test
|
public void rejectsNonScalarDeclaredResultTypesBeforeParsing() {
|
ValidationResult nullResult = validate("1 +", slots(), FormulaType.NULL);
|
ValidationResult listResult = validate("1 +", slots(), FormulaType.DECIMAL_LIST);
|
|
assertError(nullResult, "INVALID_RESULT_TYPE", -1, -1);
|
assertError(listResult, "INVALID_RESULT_TYPE", -1, -1);
|
}
|
|
@Test
|
public void validatesRoundingPairScaleModeAndResultApplicability() {
|
assertError(service.validate(request("1", slots(), FormulaType.DECIMAL, 2, null)),
|
"INVALID_ROUNDING_CONFIGURATION", -1, -1);
|
assertError(service.validate(request("1", slots(), FormulaType.DECIMAL, null, "HALF_UP")),
|
"INVALID_ROUNDING_CONFIGURATION", -1, -1);
|
assertError(service.validate(request("1", slots(), FormulaType.DECIMAL, -1, "HALF_UP")),
|
"INVALID_ROUNDING_SCALE", -1, -1);
|
assertError(service.validate(request("1", slots(), FormulaType.DECIMAL, 13, "HALF_UP")),
|
"INVALID_ROUNDING_SCALE", -1, -1);
|
assertError(service.validate(request("1", slots(), FormulaType.DECIMAL, 2, "CEILING")),
|
"UNSUPPORTED_ROUNDING_MODE", -1, -1);
|
assertError(service.validate(request("\"text\"", slots(), FormulaType.TEXT, 2, "DOWN")),
|
"ROUNDING_NOT_ALLOWED", -1, -1);
|
assertError(service.validate(request("true", slots(), FormulaType.BOOLEAN, 0, "UP")),
|
"ROUNDING_NOT_ALLOWED", -1, -1);
|
|
ValidationResult valid = service.validate(
|
request("1 / 2", slots(), FormulaType.DECIMAL, 2, "HALF_EVEN"));
|
assertFalse(valid.hasErrors());
|
assertEquals("\\frac{1}{2}", valid.getLatex());
|
}
|
|
@Test
|
public void reportsSyntaxBeforeRoundingRuleErrors() {
|
ValidationResult result = service.validate(
|
request("1 +", slots(), FormulaType.DECIMAL, 13, "CEILING"));
|
|
assertError(result, "SYNTAX_ERROR", 3, 3);
|
assertEquals(1, result.getDiagnostics().size());
|
}
|
|
@Test
|
public void validatesRequestAndSlotShapeBeforeParsing() {
|
assertError(service.validate(null), "INVALID_REQUEST", -1, -1);
|
assertError(validate(" ", slots(), FormulaType.DECIMAL),
|
"EXPRESSION_REQUIRED", 0, 3);
|
assertError(validate("1 +", null, FormulaType.DECIMAL),
|
"PARAMETERS_REQUIRED", -1, -1);
|
assertError(validate("1 +", slots(slot("ABS", "参数", FormulaType.DECIMAL)),
|
FormulaType.DECIMAL), "INVALID_PARAMETER_CODE", -1, -1);
|
assertError(validate("1 +", slots(slot("sample_a1", " ", FormulaType.DECIMAL)),
|
FormulaType.DECIMAL), "INVALID_PARAMETER_DISPLAY_NAME", -1, -1);
|
assertError(validate("1 +", slots(slot("sample_a1", "参数", FormulaType.NULL)),
|
FormulaType.DECIMAL), "INVALID_PARAMETER_TYPE", -1, -1);
|
assertError(validate("1 +", slots(slot("sample_a1", "参数1", FormulaType.DECIMAL),
|
slot("sample_a1", "参数2", FormulaType.DECIMAL)), FormulaType.DECIMAL),
|
"DUPLICATE_PARAMETER", -1, -1);
|
}
|
|
@Test
|
public void rejectsAllParserReservedCodesBeforeParsing() {
|
for (String code : Arrays.asList(
|
"true", "false", "null", "new", "class", "import", "package", "this", "super")) {
|
assertError(validate("1 +", slots(slot(code, "参数", FormulaType.DECIMAL)),
|
FormulaType.DECIMAL), "RESERVED_PARAMETER_CODE", -1, -1);
|
}
|
}
|
|
@Test
|
public void escapesAllRequiredLatexCharactersInParameterDisplayNames() {
|
ValidationResult result = validate("sample_a1",
|
slots(slot("sample_a1", "\\{}_%#&", FormulaType.DECIMAL)), FormulaType.DECIMAL);
|
|
assertFalse(result.hasErrors());
|
assertEquals("\\text{\\textbackslash{}\\{\\}\\_\\%\\#\\&}", result.getLatex());
|
}
|
|
@Test
|
public void rendersControlledFunctionsOperatorsPrecedenceAndSafeStrings() {
|
ValidationResult result = validate(
|
"IF(sample_a1 > 0 && true, POW(sample_a1 + 1, 2), \"<b>{x}_%#&</b>\")",
|
slots(slot("sample_a1", "样品", FormulaType.DECIMAL)), FormulaType.DECIMAL);
|
|
assertError(result, "TYPE_MISMATCH", 0, 66);
|
|
ValidationResult text = validate("\"<b>{x}_%#&</b>\"", slots(), FormulaType.TEXT);
|
assertFalse(text.hasErrors());
|
assertNotNull(text.getLatex());
|
assertFalse(text.getLatex().contains("<b>"));
|
assertFalse(text.getLatex().contains("</b>"));
|
assertTrue(text.getLatex().contains("\\textless{}"));
|
assertTrue(text.getLatex().contains("\\{x\\}\\_\\%\\#\\&"));
|
|
ValidationResult arithmetic = validate("1 + 2 * 3 ^ 4", slots(), FormulaType.DECIMAL);
|
assertEquals("1 + 2 \\cdot 3^{4}", arithmetic.getLatex());
|
|
ValidationResult function = validate("ABS(-1)", slots(), FormulaType.DECIMAL);
|
assertEquals("\\left|-1\\right|", function.getLatex());
|
}
|
|
@Test
|
public void rendersStandardMathematicalFunctionSymbols() {
|
java.util.List<ParameterSlot> scalar = slots(
|
slot("sample_a1", "样品", FormulaType.DECIMAL));
|
java.util.List<ParameterSlot> list = slots(
|
slot("values_a1", "重复测定值", FormulaType.DECIMAL_LIST));
|
|
assertLatex("SQRT(sample_a1)", scalar, "\\sqrt{\\text{样品}}");
|
assertLatex("POW(sample_a1 + 1, 2)", scalar,
|
"\\left(\\text{样品} + 1\\right)^{2}");
|
assertLatex("NTH_ROOT(8, 3)", slots(), "\\sqrt[3]{8}");
|
assertLatex("LN(sample_a1)", scalar, "\\ln\\left(\\text{样品}\\right)");
|
assertLatex("LOG10(sample_a1)", scalar,
|
"\\log_{10}\\left(\\text{样品}\\right)");
|
assertLatex("LOG(sample_a1, 2)", scalar,
|
"\\log_{2}\\left(\\text{样品}\\right)");
|
assertLatex("EXP(sample_a1)", scalar, "e^{\\text{样品}}");
|
assertLatex("MIN(1, 2)", slots(), "\\min\\left(1, 2\\right)");
|
assertLatex("MAX(1, 2)", slots(), "\\max\\left(1, 2\\right)");
|
assertLatex("SUM(values_a1)", list,
|
"\\sum\\left(\\text{重复测定值}\\right)");
|
assertLatex("MEAN(values_a1)", list, "\\overline{\\text{重复测定值}}");
|
assertLatex("STDDEV_S(values_a1)", list,
|
"s\\left(\\text{重复测定值}\\right)");
|
assertLatex("STDDEV_P(values_a1)", list,
|
"\\sigma\\left(\\text{重复测定值}\\right)");
|
assertLatex("VAR_S(values_a1)", list,
|
"s^{2}\\left(\\text{重复测定值}\\right)");
|
assertLatex("VAR_P(values_a1)", list,
|
"\\sigma^{2}\\left(\\text{重复测定值}\\right)");
|
}
|
|
@Test
|
public void preservesGroupingInNestedMathematicalFunctions() {
|
java.util.List<ParameterSlot> scalar = slots(
|
slot("sample_a1", "样品", FormulaType.DECIMAL));
|
|
assertLatex("POW(sample_a1 + 1, NTH_ROOT(8, 3))", scalar,
|
"\\left(\\text{样品} + 1\\right)^{\\sqrt[3]{8}}");
|
assertLatex("LOG(EXP(sample_a1), 10)", scalar,
|
"\\log_{10}\\left(e^{\\text{样品}}\\right)");
|
}
|
|
@Test
|
public void preservesLeftNestedEqualityGrouping() {
|
ValidationResult equal = validate("(true == false) == false", slots(), FormulaType.BOOLEAN);
|
assertEquals("\\left(\\mathrm{true} = \\mathrm{false}\\right) = \\mathrm{false}",
|
equal.getLatex());
|
|
ValidationResult mixed = validate("(true != false) == true", slots(), FormulaType.BOOLEAN);
|
assertEquals("\\left(\\mathrm{true} \\ne \\mathrm{false}\\right) = \\mathrm{true}",
|
mixed.getLatex());
|
}
|
|
@Test
|
public void neverEmitsDangerousLatexCommandsOrRawHtml() {
|
ValidationResult result = validate("payload_a1",
|
slots(slot("payload_a1", "\\href{a}{b}<img>", FormulaType.TEXT)), FormulaType.TEXT);
|
|
assertFalse(result.hasErrors());
|
assertFalse(result.getLatex().contains("\\href"));
|
assertFalse(result.getLatex().contains("\\html"));
|
assertFalse(result.getLatex().contains("\\include"));
|
assertFalse(result.getLatex().contains("<img>"));
|
}
|
|
@Test
|
public void rendererRejectsFunctionNamesOutsideRegistry() {
|
FormulaAst.FunctionCall call = new FormulaAst.FunctionCall(0, 1,
|
"X}\\href{javascript:1}{x}\\operatorname{Y", Collections.<FormulaAst.Node>emptyList());
|
|
assertRendererRejected(call);
|
}
|
|
@Test
|
public void rendererRejectsUnknownUnaryAndBinaryOperators() {
|
FormulaAst.Literal one = new FormulaAst.Literal(
|
0, 1, FormulaAst.LiteralKind.DECIMAL, BigDecimal.ONE);
|
|
assertRendererRejected(new FormulaAst.Unary(0, 1, "~", one));
|
assertRendererRejected(new FormulaAst.Binary(0, 1, "\\href", one, one));
|
}
|
|
@Test
|
public void syntaxFailureDoesNotReuseLatexFromPreviousRequest() {
|
ValidationResult valid = validate("1 / 2", slots(), FormulaType.DECIMAL);
|
assertEquals("\\frac{1}{2}", valid.getLatex());
|
|
ValidationResult invalid = validate("1 +", slots(), FormulaType.DECIMAL);
|
assertError(invalid, "SYNTAX_ERROR", 3, 3);
|
assertNull(invalid.getLatex());
|
}
|
|
private ValidationResult validate(String expression, java.util.List<ParameterSlot> parameters,
|
FormulaType resultType) {
|
return service.validate(request(expression, parameters, resultType, null, null));
|
}
|
|
private static FormulaValidationRequest request(String expression,
|
java.util.List<ParameterSlot> parameters,
|
FormulaType resultType,
|
Integer scale, String mode) {
|
return new FormulaValidationRequest(expression, parameters, resultType, scale, mode);
|
}
|
|
private static ParameterSlot slot(String code, String displayName, FormulaType type) {
|
return new ParameterSlot(code, displayName, type);
|
}
|
|
private static java.util.List<ParameterSlot> slots(ParameterSlot... values) {
|
return values.length == 0 ? Collections.<ParameterSlot>emptyList() : Arrays.asList(values);
|
}
|
|
private static void assertError(ValidationResult result, String code, int start, int end) {
|
assertTrue("expected error " + code, result.hasErrors());
|
assertDiagnostic(result, FormulaDiagnostic.Severity.ERROR, code, start, end);
|
assertNull(result.getLatex());
|
}
|
|
private static void assertDiagnostic(ValidationResult result,
|
FormulaDiagnostic.Severity severity,
|
String code, int start, int end) {
|
for (FormulaDiagnostic diagnostic : result.getDiagnostics()) {
|
if (diagnostic.getSeverity() == severity && code.equals(diagnostic.getCode())) {
|
assertEquals(start, diagnostic.getStart());
|
assertEquals(end, diagnostic.getEnd());
|
assertNotNull(diagnostic.getMessage());
|
return;
|
}
|
}
|
throw new AssertionError("missing diagnostic " + severity + " " + code);
|
}
|
|
private static void assertRendererRejected(FormulaAst.Node node) {
|
try {
|
new FormulaLatexRenderer().render(node, Collections.<String, String>emptyMap());
|
throw new AssertionError("expected renderer rejection");
|
} catch (IllegalArgumentException expected) {
|
assertNotNull(expected.getMessage());
|
}
|
}
|
|
private void assertLatex(String expression, java.util.List<ParameterSlot> parameters,
|
String expected) {
|
ValidationResult result = validate(expression, parameters, FormulaType.DECIMAL);
|
String diagnostics = result.getDiagnostics().isEmpty() ? ""
|
: result.getDiagnostics().get(0).getCode() + ": "
|
+ result.getDiagnostics().get(0).getMessage() + " @"
|
+ result.getDiagnostics().get(0).getStart() + ".."
|
+ result.getDiagnostics().get(0).getEnd();
|
assertFalse(diagnostics, result.hasErrors());
|
assertEquals(expected, result.getLatex());
|
}
|
}
|