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 351be5dde1 [SONAR] some coverage improvements and fixes (#6993)
351be5dde1 is described below

commit 351be5dde1d48c94c780d5f0436fa63f62d41209
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Apr 13 19:47:36 2026 +0200

    [SONAR] some coverage improvements and fixes (#6993)
---
 .../config/DescribedVariablesConfigFileTest.java   |  36 ++++
 .../core/exception/HopConversionExceptionTest.java |  54 +++++
 .../hop/core/exception/HopExceptionTest.java       |  58 ++++++
 .../hop/core/exception/HopFileExceptionTest.java   |  44 ++++
 .../apache/hop/core/gui/PointAndDPointTest.java    |  79 ++++++++
 .../org/apache/hop/core/gui/RectangleTest.java     |  65 ++++++
 .../hop/core/injection/DataTypeConverterTest.java  |  53 +++++
 .../DefaultInjectionTypeConverterTest.java         | 147 ++++++++++++++
 .../core/injection/InjectionTypeConverterTest.java |  43 ++++
 .../core/injection/NullNumberConverterTest.java    |  49 +++++
 .../apache/hop/core/io/CountingStreamsTest.java    |  86 ++++++++
 .../java/org/apache/hop/core/json/HopJsonTest.java |  60 ++++++
 .../apache/hop/core/search/SearchQueryTest.java    |  73 +++++++
 .../apache/hop/core/search/SearchResultTest.java   |  82 ++++++++
 .../hop/version/EnvironmentVariableGetterTest.java |  48 +++++
 .../org/apache/hop/version/ManifestGetterTest.java |  47 +++++
 .../javascript/ScriptValuesAddedFunctions.java     |   7 +-
 ...ptValuesAddedFunctionsRhinoIntegrationTest.java | 224 +++++++++++++++++++++
 .../javascript/ScriptValuesAddedFunctionsTest.java |  20 +-
 19 files changed, 1259 insertions(+), 16 deletions(-)

diff --git 
a/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
 
b/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
new file mode 100644
index 0000000000..ee94d989e2
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.core.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import org.junit.jupiter.api.Test;
+
+class DescribedVariablesConfigFileTest {
+
+  @Test
+  void filenameAndSerializer() {
+    DescribedVariablesConfigFile file = new 
DescribedVariablesConfigFile("/tmp/hop-config.json");
+    assertEquals("/tmp/hop-config.json", file.getConfigFilename());
+    assertInstanceOf(ConfigFileSerializer.class, file.getSerializer());
+
+    file.setConfigFilename("/other/path.json");
+    assertEquals("/other/path.json", file.getConfigFilename());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/exception/HopConversionExceptionTest.java
 
b/core/src/test/java/org/apache/hop/core/exception/HopConversionExceptionTest.java
new file mode 100644
index 0000000000..c6c1a4a8c6
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/exception/HopConversionExceptionTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.core.exception;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.junit.jupiter.api.Test;
+
+class HopConversionExceptionTest {
+
+  @Test
+  void constructorStoresCausesFieldsAndRow() {
+    List<Exception> causes = Collections.singletonList(new 
NumberFormatException("bad"));
+    List<IValueMeta> fields = Collections.singletonList(new 
ValueMetaString("f1"));
+    Object[] row = new Object[] {null, "ok"};
+    HopConversionException ex =
+        new HopConversionException("conversion failed", causes, fields, row);
+    assertEquals("conversion failed", ex.getSuperMessage());
+    assertEquals(causes, ex.getCauses());
+    assertEquals(fields, ex.getFields());
+    assertArrayEquals(row, ex.getRowData());
+    assertTrue(ex.getMessage().contains("conversion failed"));
+  }
+
+  @Test
+  void defaultConstructor() {
+    HopConversionException ex = new HopConversionException();
+    assertNull(ex.getCauses());
+    assertNull(ex.getFields());
+    assertNull(ex.getRowData());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/exception/HopExceptionTest.java 
b/core/src/test/java/org/apache/hop/core/exception/HopExceptionTest.java
new file mode 100644
index 0000000000..78543936ed
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/exception/HopExceptionTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.core.exception;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.Const;
+import org.junit.jupiter.api.Test;
+
+class HopExceptionTest {
+
+  @Test
+  void getSuperMessageReturnsRawMessageWithoutFormatting() {
+    HopException ex = new HopException("hello");
+    assertEquals("hello", ex.getSuperMessage());
+  }
+
+  @Test
+  void getMessageAppendsCauseMessage() {
+    Throwable cause = new RuntimeException("root");
+    HopException ex = new HopException("top", cause);
+    String msg = ex.getMessage();
+    assertTrue(msg.contains(Const.CR));
+    assertTrue(msg.contains("top"));
+    assertTrue(msg.contains("root"));
+  }
+
+  @Test
+  void noArgConstructor() {
+    HopException ex = new HopException();
+    assertNull(ex.getSuperMessage());
+  }
+
+  @Test
+  void causeOnlyConstructor() {
+    Throwable inner = new IllegalStateException("state");
+    HopException ex = new HopException(inner);
+    assertEquals(inner, ex.getCause());
+    assertTrue(ex.getMessage().contains("state"));
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/exception/HopFileExceptionTest.java 
b/core/src/test/java/org/apache/hop/core/exception/HopFileExceptionTest.java
new file mode 100644
index 0000000000..82b1b3d722
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/exception/HopFileExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.core.exception;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.Const;
+import org.junit.jupiter.api.Test;
+
+class HopFileExceptionTest {
+
+  @Test
+  void messageAndCauseFormattedLikeHopException() {
+    Throwable cause = new RuntimeException("io failed");
+    HopFileException ex = new HopFileException("read error", cause);
+    String msg = ex.getMessage();
+    assertTrue(msg.contains(Const.CR));
+    assertTrue(msg.contains("read error"));
+    assertTrue(msg.contains("io failed"));
+    assertEquals(cause, ex.getCause());
+  }
+
+  @Test
+  void messageOnly() {
+    HopFileException ex = new HopFileException("eof");
+    assertTrue(ex.getMessage().contains("eof"));
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/gui/PointAndDPointTest.java 
b/core/src/test/java/org/apache/hop/core/gui/PointAndDPointTest.java
new file mode 100644
index 0000000000..ef9c4d8d08
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/gui/PointAndDPointTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.core.gui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+
+class PointAndDPointTest {
+
+  @Test
+  void pointMultiplyRounds() {
+    Point p = new Point(10, 11);
+    p.multiply(1.5f);
+    assertEquals(15, p.getX());
+    assertEquals(17, p.getY());
+  }
+
+  @Test
+  void pointToDoubleAndDPointToPoint() {
+    Point p = new Point(3, 7);
+    DPoint d = p.toDouble();
+    assertEquals(3.0, d.getX());
+    assertEquals(7.0, d.getY());
+    Point back = d.toPoint();
+    assertEquals(3, back.getX());
+    assertEquals(7, back.getY());
+  }
+
+  @Test
+  void dPointMultiplyAndCopyConstructors() {
+    DPoint a = new DPoint(2.0, 4.0);
+    DPoint b = new DPoint(a);
+    b.multiply(0.5);
+    assertEquals(1.0, b.getX());
+    assertEquals(2.0, b.getY());
+    assertEquals(2.0, a.getX());
+
+    DPoint fromInt = new DPoint(new Point(1, 2));
+    assertEquals(1.0, fromInt.getX());
+    assertEquals(2.0, fromInt.getY());
+  }
+
+  @Test
+  void dPointEqualsHashCodeToString() {
+    DPoint p1 = new DPoint(1.25, 2.5);
+    DPoint p2 = new DPoint(1.25, 2.5);
+    DPoint p3 = new DPoint(1.0, 2.0);
+    assertEquals(p1, p2);
+    assertEquals(p1.hashCode(), p2.hashCode());
+    assertNotEquals(p1, p3);
+    assertEquals("DPoint(1.25,2.5)", p1.toString());
+  }
+
+  @Test
+  void pointEqualsAndToString() {
+    Point p1 = new Point(4, 5);
+    Point p2 = new Point(4, 5);
+    assertEquals(p1, p2);
+    assertEquals(p1.hashCode(), p2.hashCode());
+    assertEquals("Point(4,5)", p1.toString());
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/gui/RectangleTest.java 
b/core/src/test/java/org/apache/hop/core/gui/RectangleTest.java
new file mode 100644
index 0000000000..a175b857bc
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/gui/RectangleTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.core.gui;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class RectangleTest {
+
+  @Test
+  void copyConstructorCopiesFields() {
+    Rectangle a = new Rectangle(1, 2, 3, 4);
+    Rectangle b = new Rectangle(a);
+    assertEquals(a.x, b.x);
+    assertEquals(a.y, b.y);
+    assertEquals(a.width, b.width);
+    assertEquals(a.height, b.height);
+  }
+
+  @Test
+  void containsPointInsideAndEdges() {
+    Rectangle r = new Rectangle(10, 20, 5, 6);
+    assertTrue(r.contains(10, 20));
+    assertTrue(r.contains(15, 26));
+    assertTrue(r.contains(new Point(12, 22)));
+    assertFalse(r.contains(9, 25));
+    assertFalse(r.contains(16, 25));
+  }
+
+  @Test
+  void centreAndDistance() {
+    Rectangle r1 = new Rectangle(0, 0, 10, 10);
+    Rectangle r2 = new Rectangle(10, 0, 10, 10);
+    assertEquals(5, r1.getCentreX());
+    assertEquals(5, r1.getCentreY());
+    assertEquals(15, r2.getCentreX());
+    assertEquals(5, r2.getCentreY());
+    assertEquals(10.0, r1.distance(r2), 1e-9);
+  }
+
+  @Test
+  void isEmptyAndToString() {
+    assertTrue(new Rectangle(0, 0, 0, 0).isEmpty());
+    assertFalse(new Rectangle(0, 0, 1, 0).isEmpty());
+    assertEquals("Rectangle(2,3-4x5)", new Rectangle(2, 3, 4, 5).toString());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/injection/DataTypeConverterTest.java 
b/core/src/test/java/org/apache/hop/core/injection/DataTypeConverterTest.java
new file mode 100644
index 0000000000..056e3e26c0
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/injection/DataTypeConverterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.core.injection;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaFactory;
+import org.apache.hop.junit.rules.RestoreHopEnvironmentExtension;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(RestoreHopEnvironmentExtension.class)
+class DataTypeConverterTest {
+
+  @BeforeAll
+  static void initHop() throws HopException {
+    HopClientEnvironment.init();
+  }
+
+  @Test
+  void string2intPrimitiveMapsValueMetaNameToPluginId() throws 
HopValueException {
+    DataTypeConverter c = new DataTypeConverter();
+    int stringId = ValueMetaFactory.getIdForValueMeta("String");
+    assertEquals(stringId, c.string2intPrimitive("String"));
+    assertEquals(stringId, c.string2intPrimitive("string"));
+  }
+
+  @Test
+  void unknownTypeNameReturnsTypeNone() throws HopValueException {
+    DataTypeConverter c = new DataTypeConverter();
+    assertEquals(IValueMeta.TYPE_NONE, 
c.string2intPrimitive("__not_a_registered_type__"));
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/injection/DefaultInjectionTypeConverterTest.java
 
b/core/src/test/java/org/apache/hop/core/injection/DefaultInjectionTypeConverterTest.java
new file mode 100644
index 0000000000..7b01018853
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/injection/DefaultInjectionTypeConverterTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.core.injection;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.metadata.api.IEnumHasCodeAndDescription;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DefaultInjectionTypeConverterTest {
+
+  private DefaultInjectionTypeConverter c;
+
+  @BeforeEach
+  void setUp() {
+    c = new DefaultInjectionTypeConverter();
+  }
+
+  @Test
+  void stringConversions() throws HopValueException {
+    assertEquals("hi", c.string2string("hi"));
+    assertEquals(42, c.string2intPrimitive("42"));
+    assertNull(c.string2integer(null));
+    assertEquals(7, c.string2integer("7"));
+    assertEquals(9L, c.string2longPrimitive("9"));
+    assertNull(c.string2long(null));
+    assertEquals(3L, c.string2long("3"));
+  }
+
+  @Test
+  void stringToBooleanVariants() throws HopValueException {
+    assertTrue(c.string2booleanPrimitive("Y"));
+    assertTrue(c.string2booleanPrimitive("yes"));
+    assertTrue(c.string2booleanPrimitive("True"));
+    assertTrue(c.string2booleanPrimitive("1"));
+    assertFalse(c.string2booleanPrimitive("N"));
+    assertFalse(c.string2booleanPrimitive("0"));
+    assertNull(c.string2boolean(null));
+    assertFalse(c.string2boolean("no"));
+  }
+
+  @Test
+  void stringToEnumByNameCodeAndDescriptionStyle() throws HopValueException {
+    assertEquals(Plain.A, c.string2enum(Plain.class, "A"));
+    assertNull(c.string2enum(Plain.class, null));
+    assertEquals(Coded.ONE, c.string2enum(Coded.class, "ONE"));
+    assertEquals(Coded.ONE, c.string2enum(Coded.class, "cd1"));
+    assertEquals(Coded.TWO, c.string2enum(Coded.class, "CD2"));
+    HopValueException ex =
+        assertThrows(HopValueException.class, () -> c.string2enum(Plain.class, 
"Z"));
+    assertTrue(ex.getMessage().contains("Unknown value"));
+  }
+
+  @Test
+  void booleanConversions() throws HopValueException {
+    assertNull(c.boolean2string(null));
+    assertEquals("Y", c.boolean2string(true));
+    assertEquals("N", c.boolean2string(false));
+    assertEquals(1, c.boolean2intPrimitive(true));
+    assertEquals(0, c.boolean2intPrimitive(false));
+    assertEquals(0, c.boolean2intPrimitive(null));
+    assertNull(c.boolean2integer(null));
+    assertEquals(1L, c.boolean2longPrimitive(true));
+    assertNull(c.boolean2long(null));
+    assertTrue(c.boolean2booleanPrimitive(true));
+    assertFalse(c.boolean2booleanPrimitive(false));
+    assertTrue(c.boolean2boolean(true));
+    assertNull(c.boolean2boolean(null));
+  }
+
+  @Test
+  void integerConversions() throws HopValueException {
+    assertNull(c.integer2string(null));
+    assertEquals("100", c.integer2string(100L));
+    assertEquals(5, c.integer2intPrimitive(5L));
+    assertNull(c.integer2integer(null));
+    assertEquals(9, c.integer2integer(9L));
+    assertEquals(12L, c.integer2longPrimitive(12L));
+    assertNull(c.integer2long(null));
+    assertFalse(c.integer2booleanPrimitive(0L));
+    assertTrue(c.integer2booleanPrimitive(2L));
+    assertNull(c.integer2boolean(null));
+  }
+
+  @Test
+  void numberConversions() throws HopValueException {
+    assertNull(c.number2string(null));
+    assertEquals("3.25", c.number2string(3.25));
+    assertEquals(2, c.number2intPrimitive(1.6));
+    assertEquals(0, c.number2intPrimitive(0.4));
+    assertNull(c.number2integer(null));
+    assertEquals(4L, c.number2longPrimitive(3.7));
+    assertNull(c.number2long(null));
+    assertFalse(c.number2booleanPrimitive(0.0));
+    assertTrue(c.number2booleanPrimitive(1.2));
+    assertNull(c.number2boolean(null));
+  }
+
+  private enum Plain {
+    A,
+    B
+  }
+
+  private enum Coded implements IEnumHasCodeAndDescription {
+    ONE("cd1", "First"),
+    TWO("CD2", "Second");
+
+    private final String code;
+    private final String description;
+
+    Coded(String code, String description) {
+      this.code = code;
+      this.description = description;
+    }
+
+    @Override
+    public String getCode() {
+      return code;
+    }
+
+    @Override
+    public String getDescription() {
+      return description;
+    }
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/injection/InjectionTypeConverterTest.java
 
b/core/src/test/java/org/apache/hop/core/injection/InjectionTypeConverterTest.java
new file mode 100644
index 0000000000..24203d2e80
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/injection/InjectionTypeConverterTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.core.injection;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.hop.core.exception.HopRuntimeException;
+import org.junit.jupiter.api.Test;
+
+class InjectionTypeConverterTest {
+
+  @Test
+  void defaultImplementationRejectsConversions() {
+    InjectionTypeConverter c = new InjectionTypeConverter();
+    assertThrows(HopRuntimeException.class, () -> c.string2string("x"));
+    assertThrows(HopRuntimeException.class, () -> c.string2intPrimitive("1"));
+    assertThrows(HopRuntimeException.class, () -> c.string2integer("1"));
+    assertThrows(HopRuntimeException.class, () -> 
c.string2booleanPrimitive("Y"));
+    assertThrows(HopRuntimeException.class, () -> 
c.string2enum(SampleEnum.class, "A"));
+    assertThrows(HopRuntimeException.class, () -> c.boolean2string(true));
+    assertThrows(HopRuntimeException.class, () -> c.integer2string(1L));
+    assertThrows(HopRuntimeException.class, () -> c.number2string(1.0));
+  }
+
+  private enum SampleEnum {
+    A
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/injection/NullNumberConverterTest.java 
b/core/src/test/java/org/apache/hop/core/injection/NullNumberConverterTest.java
new file mode 100644
index 0000000000..811af305f0
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/core/injection/NullNumberConverterTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.core.injection;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class NullNumberConverterTest {
+
+  private NullNumberConverter c;
+
+  @BeforeEach
+  void setUp() {
+    c = new NullNumberConverter();
+  }
+
+  @Test
+  void blankStringYieldsMinusOneForIntPrimitive() {
+    assertEquals(-1, c.string2intPrimitive(null));
+    assertEquals(-1, c.string2intPrimitive(""));
+    assertEquals(-1, c.string2intPrimitive("   "));
+    assertEquals(5, c.string2intPrimitive("5"));
+  }
+
+  @Test
+  void nullBoxedNumbersYieldMinusOneForIntPrimitive() {
+    assertEquals(-1, c.integer2intPrimitive(null));
+    assertEquals(-1, c.number2intPrimitive(null));
+    assertEquals(3, c.integer2intPrimitive(3L));
+    assertEquals(2, c.number2intPrimitive(1.7));
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/io/CountingStreamsTest.java 
b/core/src/test/java/org/apache/hop/core/io/CountingStreamsTest.java
new file mode 100644
index 0000000000..c1a3b34333
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/io/CountingStreamsTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.core.io;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+class CountingStreamsTest {
+
+  @Test
+  void countingInputStreamSingleByteRead() throws IOException {
+    byte[] data = {1, 2, 3};
+    try (CountingInputStream in = new CountingInputStream(new 
ByteArrayInputStream(data))) {
+      assertEquals(1, in.read());
+      assertEquals(2, in.read());
+      assertEquals(3, in.read());
+      assertEquals(-1, in.read());
+      assertEquals(3, in.getCount());
+    }
+  }
+
+  @Test
+  void countingInputStreamBulkRead() throws IOException {
+    byte[] data = {9, 8, 7, 6};
+    try (CountingInputStream in = new CountingInputStream(new 
ByteArrayInputStream(data))) {
+      byte[] buf = new byte[4];
+      int n = in.read(buf, 0, 4);
+      assertEquals(4, n);
+      assertArrayEquals(data, buf);
+      assertEquals(4, in.getCount());
+      assertEquals(-1, in.read(buf, 0, 1));
+      assertEquals(4, in.getCount());
+    }
+  }
+
+  @Test
+  void countingOutputStreamWrites() throws IOException {
+    ByteArrayOutputStream raw = new ByteArrayOutputStream();
+    try (CountingOutputStream out = new CountingOutputStream(raw)) {
+      out.write(42);
+      out.write(new byte[] {1, 2, 3}, 1, 2);
+      assertEquals(3, out.getCount());
+    }
+    assertArrayEquals(new byte[] {42, 2, 3}, raw.toByteArray());
+  }
+
+  @Test
+  void countingInputStreamEofDoesNotIncrementCount() throws IOException {
+    try (CountingInputStream in = new CountingInputStream(new 
ByteArrayInputStream(new byte[0]))) {
+      assertEquals(-1, in.read());
+      assertEquals(0, in.getCount());
+    }
+  }
+
+  @Test
+  void countingInputStreamZeroLengthReadDoesNotIncrement() throws IOException {
+    try (CountingInputStream in =
+        new CountingInputStream(new ByteArrayInputStream(new byte[] {1}))) {
+      byte[] buf = new byte[1];
+      assertEquals(0, in.read(buf, 0, 0));
+      assertEquals(0, in.getCount());
+      assertEquals(1, in.read(buf, 0, 1));
+      assertEquals(1, in.getCount());
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/json/HopJsonTest.java 
b/core/src/test/java/org/apache/hop/core/json/HopJsonTest.java
new file mode 100644
index 0000000000..25bd1fcd0c
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/json/HopJsonTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.core.json;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.junit.jupiter.api.Test;
+
+class HopJsonTest {
+
+  @Test
+  void newMapperDisablesUnknownPropertyFailureAndIndent() {
+    ObjectMapper mapper = HopJson.newMapper();
+    assertFalse(
+        mapper
+            .getDeserializationConfig()
+            .isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES));
+    
assertFalse(mapper.getSerializationConfig().isEnabled(SerializationFeature.INDENT_OUTPUT));
+  }
+
+  @Test
+  void newMapperIgnoresUnknownJsonProperties() throws Exception {
+    ObjectMapper mapper = HopJson.newMapper();
+    Simple bean = mapper.readValue("{\"a\":1,\"extra\":true}", Simple.class);
+    assertEquals(1, bean.a);
+  }
+
+  @Test
+  void newMapperWritesCompactJson() throws Exception {
+    ObjectMapper mapper = HopJson.newMapper();
+    Simple bean = new Simple();
+    bean.a = 42;
+    String json = mapper.writeValueAsString(bean);
+    assertFalse(json.contains("\n"));
+    assertEquals("{\"a\":42}", json);
+  }
+
+  public static class Simple {
+    public int a;
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/core/search/SearchQueryTest.java 
b/core/src/test/java/org/apache/hop/core/search/SearchQueryTest.java
new file mode 100644
index 0000000000..4373766f0c
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/search/SearchQueryTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.core.search;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class SearchQueryTest {
+
+  @Test
+  void emptySearchStringMatchesNonEmptyTargetOnly() {
+    SearchQuery q = new SearchQuery("", false, false);
+    assertTrue(q.matches("anything"));
+    assertFalse(q.matches(""));
+  }
+
+  @Test
+  void substringCaseInsensitive() {
+    SearchQuery q = new SearchQuery("Foo", false, false);
+    assertTrue(q.matches("barfooBaz"));
+    assertFalse(q.matches("barf00Baz"));
+  }
+
+  @Test
+  void substringCaseSensitive() {
+    SearchQuery q = new SearchQuery("Foo", true, false);
+    assertTrue(q.matches("xFooy"));
+    assertFalse(q.matches("xfooy"));
+  }
+
+  @Test
+  void regExCaseInsensitive() {
+    SearchQuery q = new SearchQuery("^a.c$", false, true);
+    assertTrue(q.matches("AbC"));
+    assertFalse(q.matches("axxc"));
+  }
+
+  @Test
+  void regExCaseSensitive() {
+    SearchQuery q = new SearchQuery("^ABC$", true, true);
+    assertTrue(q.matches("ABC"));
+    assertFalse(q.matches("abc"));
+  }
+
+  @Test
+  void gettersSetters() {
+    SearchQuery q = new SearchQuery();
+    q.setSearchString("x");
+    q.setCaseSensitive(true);
+    q.setRegEx(true);
+    assertEquals("x", q.getSearchString());
+    assertTrue(q.isCaseSensitive());
+    assertTrue(q.isRegEx());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/core/search/SearchResultTest.java 
b/core/src/test/java/org/apache/hop/core/search/SearchResultTest.java
new file mode 100644
index 0000000000..5022cbac03
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/search/SearchResultTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.core.search;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class SearchResultTest {
+
+  private static final class DummySearchable implements ISearchable<String> {
+    @Override
+    public String getLocation() {
+      return "loc";
+    }
+
+    @Override
+    public String getName() {
+      return "n";
+    }
+
+    @Override
+    public String getType() {
+      return "t";
+    }
+
+    @Override
+    public String getFilename() {
+      return "f.hpl";
+    }
+
+    @Override
+    public String getSearchableObject() {
+      return "obj";
+    }
+
+    @Override
+    public ISearchableCallback getSearchCallback() {
+      return null;
+    }
+  }
+
+  @Test
+  void accessorsAndMutators() {
+    DummySearchable searchable = new DummySearchable();
+    SearchResult r = new SearchResult(searchable, "match", "desc", 
"transform", "value");
+
+    assertEquals(searchable, r.getMatchingSearchable());
+    assertEquals("match", r.getMatchingString());
+    assertEquals("desc", r.getDescription());
+    assertEquals("transform", r.getComponent());
+    assertEquals("value", r.getValue());
+
+    r.setMatchingString("m2");
+    r.setDescription("d2");
+    r.setComponent("c2");
+    r.setValue("v2");
+    r.setMatchingSearchable(null);
+
+    assertEquals("m2", r.getMatchingString());
+    assertEquals("d2", r.getDescription());
+    assertEquals("c2", r.getComponent());
+    assertEquals("v2", r.getValue());
+    assertNull(r.getMatchingSearchable());
+  }
+}
diff --git 
a/core/src/test/java/org/apache/hop/version/EnvironmentVariableGetterTest.java 
b/core/src/test/java/org/apache/hop/version/EnvironmentVariableGetterTest.java
new file mode 100644
index 0000000000..0f05d4075d
--- /dev/null
+++ 
b/core/src/test/java/org/apache/hop/version/EnvironmentVariableGetterTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.version;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 org.apache.hop.core.exception.HopRuntimeException;
+import org.junit.jupiter.api.Test;
+
+class EnvironmentVariableGetterTest {
+
+  @Test
+  void getEnvVariable_returnsPathOnTypicalSystems() throws Exception {
+    EnvironmentVariableGetter getter = new EnvironmentVariableGetter();
+    String path = getter.getEnvVarible("PATH");
+    assertNotNull(path);
+    assertFalse(path.isEmpty());
+  }
+
+  @Test
+  void getEnvVariable_throwsWhenUndefined() {
+    EnvironmentVariableGetter getter = new EnvironmentVariableGetter();
+    HopRuntimeException ex =
+        assertThrows(
+            HopRuntimeException.class,
+            () -> 
getter.getEnvVarible("HOP_TEST_ENV_VAR_SHOULD_NOT_EXIST_9f3a2c1b"));
+    assertTrue(ex.getMessage().contains("undefined"));
+    
assertTrue(ex.getMessage().contains("HOP_TEST_ENV_VAR_SHOULD_NOT_EXIST_9f3a2c1b"));
+  }
+}
diff --git a/core/src/test/java/org/apache/hop/version/ManifestGetterTest.java 
b/core/src/test/java/org/apache/hop/version/ManifestGetterTest.java
new file mode 100644
index 0000000000..f39351d482
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/version/ManifestGetterTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.version;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.net.URL;
+import java.util.jar.Manifest;
+import org.junit.jupiter.api.Test;
+
+/**
+ * {@link ManifestGetter} only supports {@code META-INF/MANIFEST.MF} resolved 
from a {@code jar:}
+ * URL. When the first manifest on the classpath comes from a directory 
({@code file:}), the call
+ * fails with {@link ClassCastException}; this test is skipped in that case.
+ */
+class ManifestGetterTest {
+
+  @Test
+  void getManifest_readsFromJarUrlWhenApplicable() throws Exception {
+    ManifestGetter getter = new ManifestGetter();
+    URL url = getter.getClass().getResource("/META-INF/MANIFEST.MF");
+    assumeTrue(url != null, "META-INF/MANIFEST.MF not found on classpath");
+    assumeTrue(
+        "jar".equalsIgnoreCase(url.getProtocol()),
+        "ManifestGetter requires a JAR manifest URL, got: " + url);
+
+    Manifest manifest = getter.getManifest();
+    assertNotNull(manifest);
+    assertNotNull(manifest.getMainAttributes());
+  }
+}
diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
index 9aac30cf78..274f6d66e5 100644
--- 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctions.java
@@ -76,6 +76,7 @@ import org.mozilla.javascript.Scriptable;
 import org.mozilla.javascript.ScriptableObject;
 import org.mozilla.javascript.WrappedException;
 
+@SuppressWarnings({"java:S1172", "java:S100"})
 public class ScriptValuesAddedFunctions extends ScriptableObject {
   @Serial private static final long serialVersionUID = 1L;
 
@@ -312,12 +313,11 @@ public class ScriptValuesAddedFunctions extends 
ScriptableObject {
         Calendar fisOffsetDate = Calendar.getInstance();
         startDate.setTime(dIn);
         Format dfFormatter = new SimpleDateFormat("dd.MM.yyyy");
-        String strOffsetDate =
-            Context.toString(argList[1]) + 
String.valueOf(startDate.get(Calendar.YEAR));
+        String strOffsetDate = Context.toString(argList[1]) + 
startDate.get(Calendar.YEAR);
         Date dOffset = (Date) dfFormatter.parseObject(strOffsetDate);
         fisOffsetDate.setTime(dOffset);
 
-        String strFisStartDate = "01.01." + 
String.valueOf(startDate.get(Calendar.YEAR) + 1);
+        String strFisStartDate = "01.01." + startDate.get(Calendar.YEAR) + 1;
         fisStartDate.setTime((Date) dfFormatter.parseObject(strFisStartDate));
         int iDaysToAdd =
             (int) ((startDate.getTimeInMillis() - 
fisOffsetDate.getTimeInMillis()) / 86400000);
@@ -1792,6 +1792,7 @@ public class ScriptValuesAddedFunctions extends 
ScriptableObject {
   }
 
   // Print
+  @SuppressWarnings("java:S106")
   public static void print(
       Context actualContext, Scriptable actualObject, Object[] argList, 
Function functionContext) {
     for (Object o : argList) { // don't worry about "undefined" arguments
diff --git 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsRhinoIntegrationTest.java
 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsRhinoIntegrationTest.java
new file mode 100644
index 0000000000..62a064eb84
--- /dev/null
+++ 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsRhinoIntegrationTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.javascript;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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.Date;
+import org.apache.hop.core.Const;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.EvaluatorException;
+import org.mozilla.javascript.Scriptable;
+
+/**
+ * Exercises {@link ScriptValuesAddedFunctions} through the same Rhino entry 
points the JavaScript
+ * transform uses ({@link Context#toString}, {@link Context#toNumber}, {@link 
Context#jsToJava}).
+ */
+class ScriptValuesAddedFunctionsRhinoIntegrationTest {
+
+  private Context cx;
+  private Scriptable scope;
+
+  @BeforeEach
+  void enterRhino() {
+    cx = Context.enter();
+    scope = cx.initStandardObjects();
+  }
+
+  @AfterEach
+  void exitRhino() {
+    Context.exit();
+  }
+
+  private Object[] args(Object... o) {
+    return o;
+  }
+
+  @Test
+  void getDigitsOnly_stripsNonDigits() {
+    assertEquals("123", ScriptValuesAddedFunctions.getDigitsOnly(cx, scope, 
args("a1b2c3"), null));
+  }
+
+  @Test
+  void getDigitsOnly_requiresOneArgument() {
+    assertThrows(
+        EvaluatorException.class,
+        () -> ScriptValuesAddedFunctions.getDigitsOnly(cx, scope, args(), 
null));
+  }
+
+  @Test
+  void luhnCheck_validAndInvalid() {
+    assertTrue(ScriptValuesAddedFunctions.LuhnCheck(cx, scope, 
args("79927398713"), null));
+    assertTrue(ScriptValuesAddedFunctions.LuhnCheck(cx, scope, args("0"), 
null));
+    assertFalse(ScriptValuesAddedFunctions.LuhnCheck(cx, scope, 
args("79927398712"), null));
+    assertFalse(ScriptValuesAddedFunctions.LuhnCheck(cx, scope, 
args("not-a-number"), null));
+  }
+
+  @Test
+  void indexOf_twoAndThreeArguments() {
+    assertEquals(2, ScriptValuesAddedFunctions.indexOf(cx, scope, 
args("hello", "l"), null));
+    assertEquals(3, ScriptValuesAddedFunctions.indexOf(cx, scope, 
args("hello", "l", 3), null));
+  }
+
+  @Test
+  void trim_ltrim_rtrim() {
+    assertEquals("x", ScriptValuesAddedFunctions.trim(cx, scope, args("  x  
"), null));
+    assertEquals("x  ", ScriptValuesAddedFunctions.ltrim(cx, scope, args("  x  
"), null));
+    assertEquals("  x", ScriptValuesAddedFunctions.rtrim(cx, scope, args("  x  
"), null));
+  }
+
+  @Test
+  void substr_twoAndThreeArguments() {
+    assertEquals("bcd", ScriptValuesAddedFunctions.substr(cx, scope, 
args("abcd", 1), null));
+    assertEquals("bc", ScriptValuesAddedFunctions.substr(cx, scope, 
args("abcd", 1, 2), null));
+  }
+
+  @Test
+  void fillString_lpad_rpad() {
+    assertEquals("xxx", ScriptValuesAddedFunctions.fillString(cx, scope, 
args("x", 3), null));
+    assertEquals("00ab", ScriptValuesAddedFunctions.lpad(cx, scope, args("ab", 
"0", 4), null));
+    assertEquals("ab00", ScriptValuesAddedFunctions.rpad(cx, scope, args("ab", 
"0", 4), null));
+  }
+
+  @Test
+  void upper_lower() {
+    assertEquals("AB", ScriptValuesAddedFunctions.upper(cx, scope, args("aB"), 
null));
+    assertEquals("ab", ScriptValuesAddedFunctions.lower(cx, scope, args("Ab"), 
null));
+  }
+
+  @Test
+  void escapeAndUnescapeHelpers_delegateToConst() {
+    String raw = "<tag>&\"'";
+    assertEquals(
+        Const.escapeXml(raw), ScriptValuesAddedFunctions.escapeXml(cx, scope, 
args(raw), null));
+    assertEquals(
+        Const.escapeHtml(raw), ScriptValuesAddedFunctions.escapeHtml(cx, 
scope, args(raw), null));
+    String escaped = Const.escapeHtml(raw);
+    assertEquals(
+        Const.unEscapeHtml(escaped),
+        ScriptValuesAddedFunctions.unEscapeHtml(cx, scope, args(escaped), 
null));
+    String xmlEsc = Const.escapeXml(raw);
+    assertEquals(
+        Const.unEscapeXml(xmlEsc),
+        ScriptValuesAddedFunctions.unEscapeXml(cx, scope, args(xmlEsc), null));
+    assertEquals(
+        Const.escapeSql("O'Brien"),
+        ScriptValuesAddedFunctions.escapeSql(cx, scope, args("O'Brien"), 
null));
+    assertEquals(
+        Const.protectXmlCdata("]]>"),
+        ScriptValuesAddedFunctions.protectXmlCdata(cx, scope, args("]]>"), 
null));
+    assertEquals(
+        Const.removeDigits("a1b2"),
+        ScriptValuesAddedFunctions.removeDigits(cx, scope, args("a1b2"), 
null));
+  }
+
+  @Test
+  void initCap_removeCRLF_getOcuranceString() {
+    assertNotNull(ScriptValuesAddedFunctions.initCap(cx, scope, args("hello 
world"), null));
+    assertEquals("ab", ScriptValuesAddedFunctions.removeCRLF(cx, scope, 
args("a\r\nb"), null));
+    assertEquals(
+        Const.getOcuranceString("banana", "a"),
+        ScriptValuesAddedFunctions.getOcuranceString(cx, scope, args("banana", 
"a"), null));
+  }
+
+  @Test
+  void trunc_floorsNumber() {
+    Object out = ScriptValuesAddedFunctions.trunc(cx, scope, args(-3.7), null);
+    assertInstanceOf(Double.class, out);
+    assertEquals(-4.0, (Double) out, 1e-9);
+  }
+
+  @Test
+  void abs_ceil_floor() {
+    assertEquals(2.5, (Double) ScriptValuesAddedFunctions.abs(cx, scope, 
args(-2.5), null), 1e-9);
+    assertEquals(3.0, (Double) ScriptValuesAddedFunctions.ceil(cx, scope, 
args(2.1), null), 1e-9);
+    assertEquals(2.0, (Double) ScriptValuesAddedFunctions.floor(cx, scope, 
args(2.9), null), 1e-9);
+  }
+
+  @Test
+  void num2str_and_str2num_roundTripInteger() {
+    String s = ScriptValuesAddedFunctions.num2str(cx, scope, args(42.0), null);
+    assertNotNull(s);
+    Object n = ScriptValuesAddedFunctions.str2num(cx, scope, args(s), null);
+    assertInstanceOf(Double.class, n);
+    assertEquals(42.0, (Double) n, 1e-9);
+  }
+
+  @Test
+  void isNum_isDate() {
+    assertEquals(Boolean.TRUE, ScriptValuesAddedFunctions.isNum(cx, scope, 
args(1.0), null));
+    assertEquals(Boolean.FALSE, ScriptValuesAddedFunctions.isNum(cx, scope, 
args("x"), null));
+    Object jsDate = Context.javaToJS(new Date(), scope);
+    assertEquals(Boolean.TRUE, ScriptValuesAddedFunctions.isDate(cx, scope, 
args(jsDate), null));
+    assertEquals(Boolean.FALSE, ScriptValuesAddedFunctions.isDate(cx, scope, 
args("nope"), null));
+  }
+
+  @Test
+  void decode_and_replace() {
+    assertEquals(
+        "yes", ScriptValuesAddedFunctions.decode(cx, scope, args("k1", "k1", 
"yes", "no"), null));
+    assertEquals("d", ScriptValuesAddedFunctions.decode(cx, scope, args("a", 
"b", "c", "d"), null));
+    assertEquals(
+        "xby", ScriptValuesAddedFunctions.replace(cx, scope, args("aby", "ab", 
"xb"), null));
+  }
+
+  @Test
+  void isEmpty_and_isMailValid() {
+    assertEquals(Boolean.TRUE, ScriptValuesAddedFunctions.isEmpty(cx, scope, 
args(""), null));
+    assertEquals(Boolean.FALSE, ScriptValuesAddedFunctions.isMailValid(cx, 
scope, args(""), null));
+    assertEquals(
+        Boolean.FALSE,
+        ScriptValuesAddedFunctions.isMailValid(cx, scope, 
args("not-an-email"), null));
+    assertEquals(
+        Boolean.TRUE, ScriptValuesAddedFunctions.isMailValid(cx, scope, 
args("[email protected]"), null));
+  }
+
+  @Test
+  void isCodepage_utf8_ascii() {
+    Object ok = ScriptValuesAddedFunctions.isCodepage(cx, scope, args("abc", 
"UTF-8"), null);
+    assertEquals(Boolean.TRUE, ok);
+  }
+
+  @Test
+  void year_month_fromDate() {
+    Date d = new Date(124, 5, 15);
+    Object y = ScriptValuesAddedFunctions.year(cx, scope, 
args(Context.javaToJS(d, scope)), null);
+    Object m = ScriptValuesAddedFunctions.month(cx, scope, 
args(Context.javaToJS(d, scope)), null);
+    assertInstanceOf(Double.class, y);
+    assertInstanceOf(Double.class, m);
+    assertEquals(2024.0, (Double) y, 1e-9);
+    // Calendar.MONTH is 0-based (June -> 5), matching 
ScriptValuesAddedFunctions.month().
+    assertEquals(5.0, (Double) m, 1e-9);
+  }
+
+  @Test
+  void getDayNumber_weekMonth() {
+    Date d = new Date(124, 5, 15);
+    Object js = Context.javaToJS(d, scope);
+    assertNotNull(ScriptValuesAddedFunctions.getDayNumber(cx, scope, args(js, 
"m"), null));
+    assertNotNull(ScriptValuesAddedFunctions.getDayNumber(cx, scope, args(js, 
"w"), null));
+  }
+}
diff --git 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsTest.java
 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsTest.java
index 774f91ac63..2f68ceabee 100644
--- 
a/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsTest.java
+++ 
b/plugins/transforms/javascript/src/test/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesAddedFunctionsTest.java
@@ -18,7 +18,7 @@
 package org.apache.hop.pipeline.transforms.javascript;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -26,6 +26,7 @@ import java.util.Calendar;
 import java.util.Date;
 import org.apache.commons.lang3.time.DateFormatUtils;
 import org.junit.jupiter.api.Test;
+import org.mozilla.javascript.EvaluatorException;
 
 class ScriptValuesAddedFunctionsTest {
   private static final String FORMATTER = "yyyy-MM-dd HH:mm:ss";
@@ -67,17 +68,10 @@ class ScriptValuesAddedFunctionsTest {
     assertEquals(0, c2.get(Calendar.MILLISECOND));
     assertEquals("2025-02-15 11:11:11", DateFormatUtils.format(rtn, 
FORMATTER));
 
-    try {
-      ScriptValuesAddedFunctions.truncDate(rtn, 6); // Should throw exception
-      fail("Expected exception - passed in level > 5 to truncDate");
-    } catch (Exception expected) {
-      // Should get here
-    }
-    try {
-      ScriptValuesAddedFunctions.truncDate(rtn, -7); // Should throw exception
-      fail("Expected exception - passed in level < 0  to truncDate");
-    } catch (Exception expected) {
-      // Should get here
-    }
+    final Date truncatedToMs = rtn;
+    assertThrows(
+        EvaluatorException.class, () -> 
ScriptValuesAddedFunctions.truncDate(truncatedToMs, 6));
+    assertThrows(
+        EvaluatorException.class, () -> 
ScriptValuesAddedFunctions.truncDate(truncatedToMs, -7));
   }
 }

Reply via email to