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), \"{x}_%#&\")", slots(slot("sample_a1", "样品", FormulaType.DECIMAL)), FormulaType.DECIMAL); assertError(result, "TYPE_MISMATCH", 0, 66); ValidationResult text = validate("\"{x}_%#&\"", slots(), FormulaType.TEXT); assertFalse(text.hasErrors()); assertNotNull(text.getLatex()); assertFalse(text.getLatex().contains("")); assertFalse(text.getLatex().contains("")); 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 scalar = slots( slot("sample_a1", "样品", FormulaType.DECIMAL)); java.util.List 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 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}", 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("")); } @Test public void rendererRejectsFunctionNamesOutsideRegistry() { FormulaAst.FunctionCall call = new FormulaAst.FunctionCall(0, 1, "X}\\href{javascript:1}{x}\\operatorname{Y", Collections.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 parameters, FormulaType resultType) { return service.validate(request(expression, parameters, resultType, null, null)); } private static FormulaValidationRequest request(String expression, java.util.List 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 slots(ParameterSlot... values) { return values.length == 0 ? Collections.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.emptyMap()); throw new AssertionError("expected renderer rejection"); } catch (IllegalArgumentException expected) { assertNotNull(expected.getMessage()); } } private void assertLatex(String expression, java.util.List 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()); } }