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