This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new bc34e15626 some cleanup, fixes and tests for the formula transform,
fixes #5696 (#5697)
bc34e15626 is described below
commit bc34e15626767935bf4822d4bdf508fff054aad7
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Sep 9 11:18:34 2025 +0200
some cleanup, fixes and tests for the formula transform, fixes #5696 (#5697)
---
.../hop/pipeline/transforms/formula/Formula.java | 46 ++----
.../pipeline/transforms/formula/FormulaMeta.java | 17 +--
.../transforms/formula/FormulaMetaFunction.java | 12 +-
.../formula/editor/util/CompletionProposal.java | 47 +------
.../formula/function/FunctionDescription.java | 134 ++----------------
.../formula/function/FunctionExample.java | 60 +-------
.../transforms/formula/util/FormulaParser.java | 2 +-
.../transforms/formula/FormulaDataTest.java | 45 ++++++
.../formula/FormulaMetaFunctionTest.java | 105 ++++++++++++++
.../transforms/formula/FormulaMetaTest.java | 122 ++++++++++++++++
.../transforms/formula/FormulaPoiTest.java | 87 ++++++++++++
.../formula/function/FunctionLibTest.java | 156 +++++++++++++++++++++
.../formula/util/FormulaFieldsExtractorTest.java | 145 +++++++++++++++++++
.../transforms/formula/util/FormulaParserTest.java | 1 +
14 files changed, 705 insertions(+), 274 deletions(-)
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
index 925084b7ca..d1eb7454c7 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/Formula.java
@@ -17,6 +17,8 @@
package org.apache.hop.pipeline.transforms.formula;
+import static
org.apache.hop.pipeline.transforms.formula.util.FormulaFieldsExtractor.getFormulaFieldList;
+
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Arrays;
@@ -33,7 +35,6 @@ import org.apache.hop.pipeline.Pipeline;
import org.apache.hop.pipeline.PipelineMeta;
import org.apache.hop.pipeline.transform.BaseTransform;
import org.apache.hop.pipeline.transform.TransformMeta;
-import org.apache.hop.pipeline.transforms.formula.util.FormulaFieldsExtractor;
import org.apache.hop.pipeline.transforms.formula.util.FormulaParser;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.CellValue;
@@ -127,7 +128,7 @@ public class Formula extends BaseTransform<FormulaMeta,
FormulaData> {
formulaFieldLists =
meta.getFormulas().stream()
.map(FormulaMetaFunction::getFormula)
- .map(FormulaFieldsExtractor::getFormulaFieldList)
+ .map(f -> getFormulaFieldList(resolve(f)))
.toArray(List[]::new);
}
@@ -233,7 +234,7 @@ public class Formula extends BaseTransform<FormulaMeta,
FormulaData> {
* class to implement your own transforms.
*
* @param transformMeta The TransformMeta object to run.
- * @param meta
+ * @param meta Formula Meta of the transform
* @param data the data object to store temporary data, database
connections, caches, result sets,
* hashtables etc.
* @param copyNr The copynumber for this transform.
@@ -272,46 +273,21 @@ public class Formula extends BaseTransform<FormulaMeta,
FormulaData> {
value = ((Number) formulaResult).doubleValue();
}
break;
- case FormulaData.RETURN_TYPE_INTEGER:
- if (fn.isNeedDataConversion()) {
- value = convertDataToTargetValueMeta(realIndex, formulaResult);
- } else {
- value = formulaResult;
- }
- break;
- case FormulaData.RETURN_TYPE_LONG:
- if (fn.isNeedDataConversion()) {
- value = convertDataToTargetValueMeta(realIndex, formulaResult);
- } else {
- value = formulaResult;
- }
- break;
- case FormulaData.RETURN_TYPE_DATE:
+ case FormulaData.RETURN_TYPE_INTEGER,
+ FormulaData.RETURN_TYPE_LONG,
+ FormulaData.RETURN_TYPE_DATE,
+ FormulaData.RETURN_TYPE_BIGDECIMAL,
+ FormulaData.RETURN_TYPE_TIMESTAMP:
if (fn.isNeedDataConversion()) {
value = convertDataToTargetValueMeta(realIndex, formulaResult);
} else {
value = formulaResult;
}
break;
- case FormulaData.RETURN_TYPE_BIGDECIMAL:
- if (fn.isNeedDataConversion()) {
- value = convertDataToTargetValueMeta(realIndex, formulaResult);
- } else {
- value = formulaResult;
- }
- break;
- case FormulaData.RETURN_TYPE_BYTE_ARRAY:
+ case FormulaData.RETURN_TYPE_BYTE_ARRAY, FormulaData.RETURN_TYPE_BOOLEAN:
value = formulaResult;
break;
- case FormulaData.RETURN_TYPE_BOOLEAN:
- value = formulaResult;
- break;
- case FormulaData.RETURN_TYPE_TIMESTAMP:
- if (fn.isNeedDataConversion()) {
- value = convertDataToTargetValueMeta(realIndex, formulaResult);
- } else {
- value = formulaResult;
- }
+ default:
break;
} // if none case is caught - null is returned.
return value;
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMeta.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMeta.java
index 8d07db9ad8..de7a9fd37b 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMeta.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMeta.java
@@ -19,6 +19,8 @@ package org.apache.hop.pipeline.transforms.formula;
import java.util.ArrayList;
import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.core.annotations.Transform;
import org.apache.hop.core.exception.HopTransformException;
import org.apache.hop.core.row.IRowMeta;
@@ -39,6 +41,8 @@ import org.apache.hop.pipeline.transform.TransformMeta;
categoryDescription =
"i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Scripting",
keywords = "i18n::Formula.keywords",
documentationUrl = "/pipeline/transforms/formula.html")
+@Getter
+@Setter
public class FormulaMeta extends BaseTransformMeta<Formula, FormulaData> {
/** The formula calculations to be performed */
@@ -58,19 +62,6 @@ public class FormulaMeta extends BaseTransformMeta<Formula,
FormulaData> {
this.formulas = m.formulas;
}
- public void setFormulas(List<FormulaMetaFunction> formulas) {
- this.formulas = formulas;
- }
-
- public List<FormulaMetaFunction> getFormulas() {
- return formulas;
- }
-
- @Override
- public Object clone() {
- return new FormulaMeta(this);
- }
-
@Override
public void getFields(
IRowMeta row,
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunction.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunction.java
index 179c5588eb..f1f178b1e2 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunction.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunction.java
@@ -66,12 +66,12 @@ public class FormulaMetaFunction {
private transient boolean needDataConversion = false;
/**
- * @param fieldName
- * @param formula
- * @param valueType
- * @param valueLength
- * @param valuePrecision
- * @param replaceField
+ * @param fieldName Output field name
+ * @param formula Formula
+ * @param valueType The value type of the return value
+ * @param valueLength Lenght of valueMeta
+ * @param valuePrecision Precision of valueMeta
+ * @param replaceField Should the source field be replaced
*/
public FormulaMetaFunction(
String fieldName,
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java
index 685a162393..27c3e49b74 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/util/CompletionProposal.java
@@ -17,6 +17,11 @@
package org.apache.hop.pipeline.transforms.formula.editor.util;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
public class CompletionProposal {
private String menuText;
private String completionString;
@@ -27,46 +32,4 @@ public class CompletionProposal {
this.completionString = completionString;
this.offset = offset;
}
-
- /**
- * @return the menuText
- */
- public String getMenuText() {
- return menuText;
- }
-
- /**
- * @param menuText the menuText to set
- */
- public void setMenuText(String menuText) {
- this.menuText = menuText;
- }
-
- /**
- * @return the completionString
- */
- public String getCompletionString() {
- return completionString;
- }
-
- /**
- * @param completionString the completionString to set
- */
- public void setCompletionString(String completionString) {
- this.completionString = completionString;
- }
-
- /**
- * @return the offset
- */
- public int getOffset() {
- return offset;
- }
-
- /**
- * @param offset the offset to set
- */
- public void setOffset(int offset) {
- this.offset = offset;
- }
}
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescription.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescription.java
index e71a6f4846..fafffa2ca4 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescription.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionDescription.java
@@ -19,11 +19,15 @@ package org.apache.hop.pipeline.transforms.formula.function;
import java.util.ArrayList;
import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.core.Const;
import org.apache.hop.core.util.Utils;
import org.apache.hop.core.xml.XmlHandler;
import org.w3c.dom.Node;
+@Getter
+@Setter
public class FunctionDescription {
public static final String XML_TAG = "function";
public static final String CONST_TD = "</td>";
@@ -38,14 +42,14 @@ public class FunctionDescription {
private List<FunctionExample> functionExamples;
/**
- * @param category
- * @param name
- * @param description
- * @param syntax
- * @param returns
- * @param constraints
- * @param semantics
- * @param functionExamples
+ * @param category function category
+ * @param name function name
+ * @param description function description
+ * @param syntax of the function
+ * @param returns type of value this function returns
+ * @param constraints limitation of this function
+ * @param semantics of the functions
+ * @param functionExamples examples of how the function can be used
*/
public FunctionDescription(
String category,
@@ -85,122 +89,10 @@ public class FunctionDescription {
}
}
- /**
- * @return the category
- */
- public String getCategory() {
- return category;
- }
-
- /**
- * @param category the category to set
- */
- public void setCategory(String category) {
- this.category = category;
- }
-
- /**
- * @return the name
- */
- public String getName() {
- return name;
- }
-
- /**
- * @param name the name to set
- */
- public void setName(String name) {
- this.name = name;
- }
-
- /**
- * @return the description
- */
- public String getDescription() {
- return description;
- }
-
- /**
- * @param description the description to set
- */
- public void setDescription(String description) {
- this.description = description;
- }
-
- /**
- * @return the syntax
- */
- public String getSyntax() {
- return syntax;
- }
-
- /**
- * @param syntax the syntax to set
- */
- public void setSyntax(String syntax) {
- this.syntax = syntax;
- }
-
- /**
- * @return the returns
- */
- public String getReturns() {
- return returns;
- }
-
- /**
- * @param returns the returns to set
- */
- public void setReturns(String returns) {
- this.returns = returns;
- }
-
- /**
- * @return the constraints
- */
- public String getConstraints() {
- return constraints;
- }
-
- /**
- * @param constraints the constraints to set
- */
- public void setConstraints(String constraints) {
- this.constraints = constraints;
- }
-
- /**
- * @return the semantics
- */
- public String getSemantics() {
- return semantics;
- }
-
- /**
- * @param semantics the semantics to set
- */
- public void setSemantics(String semantics) {
- this.semantics = semantics;
- }
-
- /**
- * @return the functionExamples
- */
- public List<FunctionExample> getFunctionExamples() {
- return functionExamples;
- }
-
- /**
- * @param functionExamples the functionExamples to set
- */
- public void setFunctionExamples(List<FunctionExample> functionExamples) {
- this.functionExamples = functionExamples;
- }
-
/**
* Create a text version of a report on this function
*
- * @return
+ * @return an HTML representation of the function description
*/
public String getHtmlReport() {
StringBuilder report = new StringBuilder(200);
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExample.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExample.java
index 2db42186b9..b625a32a44 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExample.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/function/FunctionExample.java
@@ -17,9 +17,13 @@
package org.apache.hop.pipeline.transforms.formula.function;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.hop.core.xml.XmlHandler;
import org.w3c.dom.Node;
+@Getter
+@Setter
public class FunctionExample {
public static final String XML_TAG = "example";
@@ -41,60 +45,4 @@ public class FunctionExample {
this.level = XmlHandler.getTagValue(node, "level");
this.comment = XmlHandler.getTagValue(node, "comment");
}
-
- /**
- * @return the expression
- */
- public String getExpression() {
- return expression;
- }
-
- /**
- * @param expression the expression to set
- */
- public void setExpression(String expression) {
- this.expression = expression;
- }
-
- /**
- * @return the result
- */
- public String getResult() {
- return result;
- }
-
- /**
- * @param result the result to set
- */
- public void setResult(String result) {
- this.result = result;
- }
-
- /**
- * @return the level
- */
- public String getLevel() {
- return level;
- }
-
- /**
- * @param level the level to set
- */
- public void setLevel(String level) {
- this.level = level;
- }
-
- /**
- * @return the comment
- */
- public String getComment() {
- return comment;
- }
-
- /**
- * @param comment the comment to set
- */
- public void setComment(String comment) {
- this.comment = comment;
- }
}
diff --git
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
index 54dfdf6beb..9dccc8de3f 100644
---
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
+++
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParser.java
@@ -71,7 +71,7 @@ public class FormulaParser {
}
if (getNewList) {
- this.formulaFieldList = getFormulaFieldList(formula);
+ this.formulaFieldList = getFormulaFieldList(variables.resolve(formula));
}
this.evaluator = poi.evaluator(formulaFieldList.size() + 1);
this.evaluator.evaluator().clearAllCachedResultValues();
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaDataTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaDataTest.java
new file mode 100644
index 0000000000..b55d901718
--- /dev/null
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaDataTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.formula;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+
+class FormulaDataTest {
+
+ @Test
+ void testConstantsValues() {
+ assertEquals(0, FormulaData.RETURN_TYPE_STRING);
+ assertEquals(1, FormulaData.RETURN_TYPE_NUMBER);
+ assertEquals(2, FormulaData.RETURN_TYPE_INTEGER);
+ assertEquals(3, FormulaData.RETURN_TYPE_LONG);
+ assertEquals(4, FormulaData.RETURN_TYPE_DATE);
+ assertEquals(5, FormulaData.RETURN_TYPE_BIGDECIMAL);
+ assertEquals(6, FormulaData.RETURN_TYPE_BYTE_ARRAY);
+ assertEquals(7, FormulaData.RETURN_TYPE_BOOLEAN);
+ assertEquals(9, FormulaData.RETURN_TYPE_TIMESTAMP);
+ }
+
+ @Test
+ void testConstructor() {
+ FormulaData data = new FormulaData();
+ assertNotNull(data);
+ }
+}
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunctionTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunctionTest.java
new file mode 100644
index 0000000000..34eb7b8d89
--- /dev/null
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaFunctionTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.formula;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.row.value.ValueMetaFactory;
+import org.junit.jupiter.api.Test;
+
+class FormulaMetaFunctionTest {
+
+ @Test
+ void testDefaultConstructor() {
+ FormulaMetaFunction function = new FormulaMetaFunction();
+ assertEquals(-1, function.getValueLength());
+ assertEquals(-1, function.getValuePrecision());
+ assertFalse(function.isNeedDataConversion());
+ }
+
+ @Test
+ void testParameterizedConstructor() {
+ FormulaMetaFunction function =
+ new FormulaMetaFunction(
+ "test_field",
+ "1+1",
+ ValueMetaFactory.getIdForValueMeta("Number"),
+ 10,
+ 2,
+ "replace_field",
+ true);
+
+ assertEquals("test_field", function.getFieldName());
+ assertEquals("1+1", function.getFormula());
+ assertEquals(ValueMetaFactory.getIdForValueMeta("Number"),
function.getValueType());
+ assertEquals(10, function.getValueLength());
+ assertEquals(2, function.getValuePrecision());
+ assertEquals("replace_field", function.getReplaceField());
+ assertTrue(function.isSetNa());
+ }
+
+ @Test
+ void testGettersAndSetters() {
+ FormulaMetaFunction function = new FormulaMetaFunction();
+
+ function.setFieldName("my_field");
+ assertEquals("my_field", function.getFieldName());
+
+ function.setFormula("2*3");
+ assertEquals("2*3", function.getFormula());
+
+ function.setValueType(ValueMetaFactory.getIdForValueMeta("String"));
+ assertEquals(ValueMetaFactory.getIdForValueMeta("String"),
function.getValueType());
+
+ function.setValueLength(50);
+ assertEquals(50, function.getValueLength());
+
+ function.setValuePrecision(3);
+ assertEquals(3, function.getValuePrecision());
+
+ function.setReplaceField("old_field");
+ assertEquals("old_field", function.getReplaceField());
+
+ function.setSetNa(true);
+ assertTrue(function.isSetNa());
+
+ function.setNeedDataConversion(true);
+ assertTrue(function.isNeedDataConversion());
+ }
+
+ @Test
+ void testHashCode() {
+ FormulaMetaFunction function1 =
+ new FormulaMetaFunction("field1", "formula1", 1, 10, 2, "replace1",
false);
+ FormulaMetaFunction function2 =
+ new FormulaMetaFunction("field1", "formula1", 1, 10, 2, "replace1",
false);
+ FormulaMetaFunction function3 =
+ new FormulaMetaFunction("field2", "formula2", 2, 20, 3, "replace2",
true);
+
+ assertEquals(function1.hashCode(), function2.hashCode());
+ assertNotEquals(function1.hashCode(), function3.hashCode());
+ }
+
+ @Test
+ void testXmlTag() {
+ assertEquals("formula", FormulaMetaFunction.XML_TAG);
+ }
+}
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaTest.java
new file mode 100644
index 0000000000..1bbee0cf36
--- /dev/null
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaMetaTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.formula;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FormulaMetaTest {
+
+ private FormulaMeta formulaMeta;
+
+ @BeforeEach
+ void setUp() {
+ formulaMeta = new FormulaMeta();
+ }
+
+ @Test
+ void testDefaultConstructor() {
+ assertNotNull(formulaMeta.getFormulas());
+ assertTrue(formulaMeta.getFormulas().isEmpty());
+ }
+
+ @Test
+ void testCopyConstructor() {
+ List<FormulaMetaFunction> formulas = new ArrayList<>();
+ formulas.add(new FormulaMetaFunction("test", "1+1", 1, 10, 2, "", false));
+ formulaMeta.setFormulas(formulas);
+
+ FormulaMeta copy = new FormulaMeta(formulaMeta);
+ assertEquals(formulaMeta.getFormulas(), copy.getFormulas());
+ }
+
+ @Test
+ void testGetFields_ReplaceField() throws Exception {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("field_to_replace"));
+
+ FormulaMetaFunction formula =
+ new FormulaMetaFunction(
+ "", "UPPER([field_to_replace])", 2, 20, 0, "field_to_replace",
false);
+ List<FormulaMetaFunction> formulas = new ArrayList<>();
+ formulas.add(formula);
+ formulaMeta.setFormulas(formulas);
+
+ formulaMeta.getFields(rowMeta, "transform", null, null, new Variables(),
null);
+
+ assertEquals(1, rowMeta.size());
+ assertEquals("field_to_replace", rowMeta.getValueMeta(0).getName());
+ assertEquals(2, rowMeta.getValueMeta(0).getType());
+ }
+
+ @Test
+ void testGetFields_ReplaceFieldNotFound() {
+ IRowMeta rowMeta = new RowMeta();
+ rowMeta.addValueMeta(new ValueMetaString("existing_field"));
+
+ FormulaMetaFunction formula =
+ new FormulaMetaFunction("", "1+1", 1, 10, 2, "non_existing_field",
false);
+ List<FormulaMetaFunction> formulas = new ArrayList<>();
+ formulas.add(formula);
+ formulaMeta.setFormulas(formulas);
+
+ assertThrows(
+ HopTransformException.class,
+ () -> {
+ formulaMeta.getFields(rowMeta, "transform", null, null, new
Variables(), null);
+ });
+ }
+
+ @Test
+ void testGetFields_EmptyFieldName() throws Exception {
+ IRowMeta rowMeta = new RowMeta();
+
+ FormulaMetaFunction formula = new FormulaMetaFunction("", "1+1", 1, 10, 2,
"", false);
+ List<FormulaMetaFunction> formulas = new ArrayList<>();
+ formulas.add(formula);
+ formulaMeta.setFormulas(formulas);
+
+ int originalSize = rowMeta.size();
+ formulaMeta.getFields(rowMeta, "transform", null, null, new Variables(),
null);
+
+ // No new field should be added if fieldName is empty and not replacing
+ assertEquals(originalSize, rowMeta.size());
+ }
+
+ @Test
+ void testFormulasGetterSetter() {
+ List<FormulaMetaFunction> formulas = new ArrayList<>();
+ formulas.add(new FormulaMetaFunction("test", "1+1", 1, 10, 2, "", false));
+
+ formulaMeta.setFormulas(formulas);
+ assertEquals(formulas, formulaMeta.getFormulas());
+ assertEquals(1, formulaMeta.getFormulas().size());
+ }
+}
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaPoiTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaPoiTest.java
new file mode 100644
index 0000000000..a1396ff0a4
--- /dev/null
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/FormulaPoiTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.formula;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.IOException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FormulaPoiTest {
+
+ private FormulaPoi formulaPoi;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ formulaPoi = new FormulaPoi(msg -> System.out.println(msg)); // Provide a
simple logger
+ }
+
+ @AfterEach
+ void tearDown() throws IOException {
+ if (formulaPoi != null) {
+ formulaPoi.destroy();
+ }
+ }
+
+ @Test
+ void testConstructor() {
+ assertNotNull(formulaPoi);
+ }
+
+ @Test
+ void testEvaluatorCreation() {
+ FormulaPoi.Evaluator evaluator = formulaPoi.evaluator(100);
+ assertNotNull(evaluator);
+ assertNotNull(evaluator.sheet());
+ assertNotNull(evaluator.row());
+ assertNotNull(evaluator.evaluator());
+ }
+
+ @Test
+ void testEvaluatorForManyColumns() {
+ FormulaPoi.Evaluator evaluator = formulaPoi.evaluator(500); // Forces XSS
implementation
+ assertNotNull(evaluator);
+ assertNotNull(evaluator.sheet());
+ assertNotNull(evaluator.row());
+ assertNotNull(evaluator.evaluator());
+ }
+
+ @Test
+ void testReset() {
+ FormulaPoi.Evaluator evaluator = formulaPoi.evaluator(100);
+ assertNotNull(evaluator);
+
+ // Reset should clear any cached state
+ formulaPoi.reset();
+
+ // Should still work after reset
+ FormulaPoi.Evaluator evaluator2 = formulaPoi.evaluator(100);
+ assertNotNull(evaluator2);
+ }
+
+ @Test
+ void testDestroy() throws IOException {
+ formulaPoi.evaluator(100);
+ formulaPoi.evaluator(500);
+
+ // Should not throw exception
+ formulaPoi.destroy();
+ }
+}
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java
new file mode 100644
index 0000000000..ad3e659cdf
--- /dev/null
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/function/FunctionLibTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.formula.function;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.apache.hop.core.exception.HopXmlException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class FunctionLibTest {
+
+ private FunctionLib functionLib;
+
+ @BeforeEach
+ void setUp() throws HopXmlException {
+ functionLib =
+ new
FunctionLib("/org/apache/hop/pipeline/transforms/formula/function/functions.xml");
+ }
+
+ @Test
+ void testConstructor() {
+ assertNotNull(functionLib);
+ assertNotNull(functionLib.getFunctions());
+ }
+
+ @Test
+ void testGetFunctions() {
+ List<FunctionDescription> functions = functionLib.getFunctions();
+ assertNotNull(functions);
+ assertFalse(functions.isEmpty());
+
+ // Verify we have some common functions
+ boolean hasAbs = functions.stream().anyMatch(f ->
"ABS".equals(f.getName()));
+ boolean hasSum = functions.stream().anyMatch(f ->
"SUM".equals(f.getName()));
+ assertTrue(hasAbs || hasSum, "Should contain at least one common function
like ABS or SUM");
+ }
+
+ @Test
+ void testGetFunctionNames() {
+ String[] functionNames = functionLib.getFunctionNames();
+ assertNotNull(functionNames);
+ assertTrue(functionNames.length > 0);
+
+ // Function names should be sorted
+ for (int i = 1; i < functionNames.length; i++) {
+ assertTrue(
+ functionNames[i - 1].compareTo(functionNames[i]) <= 0,
+ "Function names should be sorted alphabetically");
+ }
+ }
+
+ @Test
+ void testGetFunctionCategories() {
+ String[] categories = functionLib.getFunctionCategories();
+ assertNotNull(categories);
+ assertTrue(categories.length > 0);
+
+ // Categories should be sorted
+ for (int i = 1; i < categories.length; i++) {
+ assertTrue(
+ categories[i - 1].compareTo(categories[i]) <= 0,
+ "Categories should be sorted alphabetically");
+ }
+ }
+
+ @Test
+ void testGetFunctionsForACategory() {
+ String[] categories = functionLib.getFunctionCategories();
+ if (categories.length > 0) {
+ String firstCategory = categories[0];
+ String[] functions = functionLib.getFunctionsForACategory(firstCategory);
+ assertNotNull(functions);
+
+ for (String functionName : functions) {
+ FunctionDescription desc =
functionLib.getFunctionDescription(functionName);
+ assertEquals(firstCategory, desc.getCategory());
+ }
+ }
+ }
+
+ @Test
+ void testGetFunctionsForNonExistentCategory() {
+ String[] functions =
functionLib.getFunctionsForACategory("NON_EXISTENT_CATEGORY");
+ assertNotNull(functions);
+ assertEquals(0, functions.length);
+ }
+
+ @Test
+ void testGetFunctionDescription() {
+ String[] functionNames = functionLib.getFunctionNames();
+ if (functionNames.length > 0) {
+ String firstFunction = functionNames[0];
+ FunctionDescription desc =
functionLib.getFunctionDescription(firstFunction);
+ assertNotNull(desc);
+ assertEquals(firstFunction, desc.getName());
+ }
+ }
+
+ @Test
+ void testGetFunctionDescriptionNotFound() {
+ FunctionDescription desc =
functionLib.getFunctionDescription("NON_EXISTENT_FUNCTION");
+ assertNull(desc);
+ }
+
+ @Test
+ void testInvalidXmlFile() {
+ assertThrows(
+ HopXmlException.class,
+ () -> {
+ new FunctionLib("/non_existent_file.xml");
+ });
+ }
+
+ @Test
+ void testFunctionDescriptionProperties() {
+ List<FunctionDescription> functions = functionLib.getFunctions();
+ if (!functions.isEmpty()) {
+ FunctionDescription firstFunction = functions.get(0);
+ assertNotNull(firstFunction.getName());
+ assertNotNull(firstFunction.getCategory());
+ // Description can be null/empty for some functions
+ }
+ }
+
+ @Test
+ void testSetFunctions() {
+ List<FunctionDescription> originalFunctions = functionLib.getFunctions();
+ int originalSize = originalFunctions.size();
+
+ // Test setter
+ functionLib.setFunctions(originalFunctions);
+ assertEquals(originalSize, functionLib.getFunctions().size());
+ }
+}
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java
new file mode 100644
index 0000000000..96d839a3ac
--- /dev/null
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaFieldsExtractorTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.formula.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class FormulaFieldsExtractorTest {
+
+ @Test
+ void testSimpleFieldExtraction() {
+ String formula = "[field1] + [field2]";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(2, fields.size());
+ assertTrue(fields.contains("field1"));
+ assertTrue(fields.contains("field2"));
+ }
+
+ @Test
+ void testSingleFieldExtraction() {
+ String formula = "UPPER([name])";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(1, fields.size());
+ assertEquals("name", fields.get(0));
+ }
+
+ @Test
+ void testNoFieldsInFormula() {
+ String formula = "1 + 2 * 3";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertTrue(fields.isEmpty());
+ }
+
+ @Test
+ void testComplexFormula() {
+ String formula = "IF([status] = \"active\", [amount] * [rate], 0)";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(3, fields.size());
+ assertTrue(fields.contains("status"));
+ assertTrue(fields.contains("amount"));
+ assertTrue(fields.contains("rate"));
+ }
+
+ @Test
+ void testDuplicateFields() {
+ String formula = "[value] + [value] * 2";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(2, fields.size()); // Duplicates are included
+ assertEquals("value", fields.get(0));
+ assertEquals("value", fields.get(1));
+ }
+
+ @Test
+ void testEmptyFormula() {
+ String formula = "";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertTrue(fields.isEmpty());
+ }
+
+ @Test
+ void testFormulaWithSpaces() {
+ String formula = "[ field with spaces ] + [another_field]";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(2, fields.size());
+ assertTrue(fields.contains(" field with spaces "));
+ assertTrue(fields.contains("another_field"));
+ }
+
+ @Test
+ void testUnmatchedBrackets() {
+ String formula = "[incomplete + [complete]";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(1, fields.size());
+ assertEquals("incomplete + [complete", fields.get(0));
+ }
+
+ @Test
+ void testEmptyBrackets() {
+ String formula = "[] + [field]";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(2, fields.size());
+ assertEquals("", fields.get(0)); // Empty bracket is also captured
+ assertEquals("field", fields.get(1));
+ }
+
+ @Test
+ void testNestedFunctions() {
+ String formula = "ROUND(SQRT([value1] + [value2]), 2)";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(2, fields.size());
+ assertTrue(fields.contains("value1"));
+ assertTrue(fields.contains("value2"));
+ }
+
+ @Test
+ void testSpecialCharactersInFieldNames() {
+ String formula = "[field-with-dashes] + [field_with_underscores] +
[field.with.dots]";
+ List<String> fields = FormulaFieldsExtractor.getFormulaFieldList(formula);
+
+ assertNotNull(fields);
+ assertEquals(3, fields.size());
+ assertTrue(fields.contains("field-with-dashes"));
+ assertTrue(fields.contains("field_with_underscores"));
+ assertTrue(fields.contains("field.with.dots"));
+ }
+}
diff --git
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
index 68a721a315..29e7942b0f 100644
---
a/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
+++
b/plugins/transforms/formula/src/test/java/org/apache/hop/pipeline/transforms/formula/util/FormulaParserTest.java
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.apache.hop.pipeline.transforms.formula.util;
import static org.junit.jupiter.api.Assertions.assertEquals;