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;


Reply via email to