From 77502b6c6cfb1be4964ce84510b13375a0e82674 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 12 Feb 2026 16:07:09 +0100 Subject: [PATCH 1/3] tests: ensure parseBytesTo methods do not alter input data --- .../thingsboard/script/api/tbel/TbUtils.java | 8 +- .../script/api/tbel/TbUtilsTest.java | 95 +++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 6340b02dd0..e206d2d78d 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -898,11 +898,11 @@ public class TbUtils { public static int parseBytesToInt(byte[] data, int offset, int length, boolean bigEndian) { validationNumberByLength(data, offset, length, BYTES_LEN_INT_MAX); - var bb = ByteBuffer.allocate(4); + var bb = ByteBuffer.allocate(BYTES_LEN_INT_MAX); if (!bigEndian) { bb.order(ByteOrder.LITTLE_ENDIAN); } - bb.position(bigEndian ? 4 - length : 0); + bb.position(bigEndian ? BYTES_LEN_INT_MAX - length : 0); bb.put(data, offset, length); bb.position(0); return bb.getInt(); @@ -923,11 +923,11 @@ public class TbUtils { public static long parseBytesToUnsignedInt(byte[] data, int offset, int length, boolean bigEndian) { validationNumberByLength(data, offset, length, BYTES_LEN_INT_MAX); - ByteBuffer bb = ByteBuffer.allocate(8); + ByteBuffer bb = ByteBuffer.allocate(BYTES_LEN_LONG_MAX); if (!bigEndian) { bb.order(ByteOrder.LITTLE_ENDIAN); } - bb.position(bigEndian ? 8 - length : 0); + bb.position(bigEndian ? BYTES_LEN_LONG_MAX - length : 0); bb.put(data, offset, length); bb.position(0); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 01ccd5d579..622d298bbb 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -155,6 +155,101 @@ public class TbUtilsTest { Assertions.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false)); } + @Test + public void parseBytesToInt_doesNotChangeInputData() { + byte[] data = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}; + byte[] copy = data.clone(); + TbUtils.parseBytesToInt(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesToInt(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + TbUtils.parseBytesToUnsignedInt(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesToUnsignedInt(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + TbUtils.parseBytesToLong(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesToLong(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + TbUtils.parseBytesToFloat(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesToFloat(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + TbUtils.parseBytesIntToFloat(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesIntToFloat(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + TbUtils.parseBytesToDouble(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesToDouble(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + TbUtils.parseBytesLongToDouble(data, 0, 4, true); + Assertions.assertArrayEquals(copy, data); + TbUtils.parseBytesLongToDouble(data, 0, 4, false); + Assertions.assertArrayEquals(copy, data); + + List listData = toList(new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}); + List listCopy = new ArrayList<>(listData); + TbUtils.parseBytesToInt(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesToInt(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + + TbUtils.parseBytesToUnsignedInt(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesToUnsignedInt(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + + TbUtils.parseBytesToLong(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesToLong(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + + TbUtils.parseBytesToFloat(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesToFloat(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + + TbUtils.parseBytesIntToFloat(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesIntToFloat(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + + TbUtils.parseBytesToDouble(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesToDouble(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + + TbUtils.parseBytesLongToDouble(listData, 0, 4, true); + Assertions.assertEquals(listCopy, listData); + TbUtils.parseBytesLongToDouble(listData, 0, 4, false); + Assertions.assertEquals(listCopy, listData); + } + + @Test + public void compare_parseBytesToInt_and_parseBytesToUnsignedInt() { + byte[] data = new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}; + + // 4 bytes: parseBytesToInt returns -1, parseBytesToUnsignedInt returns 4294967295L + Assertions.assertEquals(-1, TbUtils.parseBytesToInt(data, 0, 4, true)); + Assertions.assertEquals(4294967295L, TbUtils.parseBytesToUnsignedInt(data, 0, 4, true)); + + // 2 bytes (0xFFFF): both return 65535 (no sign extension for parseBytesToInt when length < 4) + Assertions.assertEquals(65535, TbUtils.parseBytesToInt(data, 0, 2, true)); + Assertions.assertEquals(65535L, TbUtils.parseBytesToUnsignedInt(data, 0, 2, true)); + + // 2 bytes with high bit set (0x8000) + byte[] data2 = new byte[]{(byte) 0x80, (byte) 0x00}; + Assertions.assertEquals(32768, TbUtils.parseBytesToInt(data2, 0, 2, true)); + Assertions.assertEquals(32768L, TbUtils.parseBytesToUnsignedInt(data2, 0, 2, true)); + } + @Test public void toFlatMap() { ExecutionHashMap inputMap = new ExecutionHashMap<>(16, ctx); From 55e3c3f7c2bfc8c116e61d882d853d4fe240512b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Mon, 23 Feb 2026 08:37:23 +0100 Subject: [PATCH 2/3] TBEL parseBytes_Test RepeatedTest(3) --- .../server/service/script/TbelInvokeDocsIoTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 67e4ba8f32..371c622cb8 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.script; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbDate; @@ -1713,7 +1714,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } - @Test + @RepeatedTest(value = 3, name = "{displayName} {currentRepetition}/{totalRepetitions}") public void parseBytes_Test() throws ExecutionException, InterruptedException { byte[] bytesExecutionArrayList = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}; msgStr = "{}"; From 010cebe5c0d05611cb1f7bd1a56f6ac928297b0b Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Mon, 23 Feb 2026 14:26:54 +0200 Subject: [PATCH 3/3] Update TBEL to 1.2.9 to fix script execution failures on repeated runs TBEL 1.2.9 fixes two issues that caused TBEL scripts to fail or produce incorrect results when executed multiple times: 1. Thread-safety: OptimizerFactory.defaultOptimizer was not volatile, so worker threads could use DynamicOptimizer instead of the intended SafeReflectiveOptimizer, leading to intermittent script failures. 2. MethodAccessor coercion: methods with ExecutionContext parameter (e.g. bytesToExecutionArrayList) failed on re-execution because the coercion fallback path did not handle ExecutionContext injection. Also add @RepeatedTest for parseBytes_Test to verify stability. Co-Authored-By: Claude Opus 4.6 --- .../service/script/TbelInvokeDocsIoTest.java | 34 ++++++++++--------- pom.xml | 3 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 67e4ba8f32..9a3421b219 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.script; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbDate; @@ -779,7 +780,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected.toString(), actual.toString()); } - @Test + @Test public void setsCreateNewSetFromCreateSetTbMethod_Test() throws ExecutionException, InterruptedException { msgStr = """ {"list": ["B", "A", "C", "A"]} @@ -800,7 +801,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected.toString(), actual.toString()); } - @Test + @Test public void setsForeachForLoop_Test() throws ExecutionException, InterruptedException { msgStr = """ {"list": ["A", "B", "C"]} @@ -975,13 +976,13 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { ArrayList listSortDesc = new ArrayList<>(List.of("hello", "C", "B", "A", 34567, 34)); Set expectedDesc = new LinkedHashSet<>(listSortDesc); Object actual = invokeScript(evalScript(decoderStr), msgStr); - assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1").toString()); - assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set1_asc").toString()); - assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set1_desc").toString()); - assertEquals(expected.toString(), ((LinkedHashMap)actual).get("set2").toString()); - assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set3").toString()); - assertEquals(expectedAsc.toString(), ((LinkedHashMap)actual).get("set3_asc").toString()); - assertEquals(expectedDesc.toString(), ((LinkedHashMap)actual).get("set3_desc").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap) actual).get("set1").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap) actual).get("set1_asc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap) actual).get("set1_desc").toString()); + assertEquals(expected.toString(), ((LinkedHashMap) actual).get("set2").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap) actual).get("set3").toString()); + assertEquals(expectedAsc.toString(), ((LinkedHashMap) actual).get("set3_asc").toString()); + assertEquals(expectedDesc.toString(), ((LinkedHashMap) actual).get("set3_desc").toString()); } @Test @@ -1002,9 +1003,9 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { List listOrigin = new ArrayList<>(List.of("C", "B", "A", 34567, "B", "C", "hello", 34)); Set expectedSet = new LinkedHashSet<>(listOrigin); Object actual = invokeScript(evalScript(decoderStr), msgStr); - assertEquals(expectedSet.toString(), ((LinkedHashMap)actual).get("set1").toString()); - assertEquals(true, ((LinkedHashMap)actual).get("result1")); - assertEquals(false, ((LinkedHashMap)actual).get("result2")); + assertEquals(expectedSet.toString(), ((LinkedHashMap) actual).get("set1").toString()); + assertEquals(true, ((LinkedHashMap) actual).get("result1")); + assertEquals(false, ((LinkedHashMap) actual).get("result2")); } @Test @@ -1025,9 +1026,9 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { Set expectedSet = new LinkedHashSet<>(listOrigin); List expectedToList = new ArrayList<>(expectedSet); Object actual = invokeScript(evalScript(decoderStr), msgStr); - assertEquals(listOrigin.toString(), ((LinkedHashMap)actual).get("list").toString()); - assertEquals(expectedSet.toString(), ((LinkedHashMap)actual).get("set1").toString()); - assertEquals(expectedToList.toString(), ((LinkedHashMap)actual).get("tolist").toString()); + assertEquals(listOrigin.toString(), ((LinkedHashMap) actual).get("list").toString()); + assertEquals(expectedSet.toString(), ((LinkedHashMap) actual).get("set1").toString()); + assertEquals(expectedToList.toString(), ((LinkedHashMap) actual).get("tolist").toString()); } @Test @@ -1713,7 +1714,7 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } - @Test + @RepeatedTest(value = 3, name = "{displayName} {currentRepetition}/{totalRepetitions}") public void parseBytes_Test() throws ExecutionException, InterruptedException { byte[] bytesExecutionArrayList = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}; msgStr = "{}"; @@ -2821,5 +2822,6 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { } return list; } + } diff --git a/pom.xml b/pom.xml index b69540231e..a28a5ff6ab 100755 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 3.9.3 3.25.5 1.76.0 - 1.2.8 + 1.2.9 1.18.38 1.2.5 1.2.5 @@ -837,6 +837,7 @@ **/test/resources/lwm2m/** **/resources/lwm2m/models/** src/main/data/resources/** + .claude/** JAVADOC_STYLE