diff --git a/application/pom.xml b/application/pom.xml index d2cc11f069..805194c408 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -458,6 +458,7 @@ thingsboard + ${project.version} **/nosql/*Test.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 9ecfd2be08..3003c046b9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -26,6 +26,7 @@ import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.edge.rpc.EdgeVersionComparator; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; import org.thingsboard.rule.engine.ai.TbAiNode; import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; @@ -281,7 +282,7 @@ public class EdgeMsgConstructorUtils { public static String getEntityAndFixLwm2mBootstrapShortServerId(DeviceProfile deviceProfile, EdgeVersion edgeVersion) { DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber()) { + if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || EdgeVersionComparator.INSTANCE.compare(edgeVersion, EdgeVersion.V_4_3_0) >= 0) { return JacksonUtil.toString(deviceProfile); } JsonNode jsonNode = JacksonUtil.valueToTree(deviceProfile); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java index 5f11abb478..bc085afc08 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java @@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.edge.rpc.EdgeVersionComparator; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -116,7 +117,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i } private boolean isValidCfToSend(CalculatedFieldType type, EdgeVersion edgeVersion) { - return edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber() || (type == SIMPLE || type == SCRIPT); + return EdgeVersionComparator.INSTANCE.compare(edgeVersion, EdgeVersion.V_4_3_0) >= 0 || (type == SIMPLE || type == SCRIPT); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index b2b72db5ad..ca35b30e59 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -16,13 +16,14 @@ package org.thingsboard.server.service.edge.rpc.utils; import lombok.extern.slf4j.Slf4j; +import org.thingsboard.edge.rpc.EdgeVersionComparator; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @Slf4j public final class EdgeVersionUtils { public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) { - return currentVersion.ordinal() < requiredVersion.ordinal(); + return EdgeVersionComparator.INSTANCE.compare(currentVersion, requiredVersion) < 0; } } diff --git a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java index 0872e21dae..c23c59d137 100644 --- a/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java @@ -205,6 +205,15 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest CustomInterval customInterval = new CustomInterval(TZ, 0L, 2L); createConsumptionCF(device.getId(), customInterval, null); + long interval = customInterval.getCurrentIntervalDurationMillis(); + + // Wait for a fresh interval + long initialIntervalStart = customInterval.getCurrentIntervalStartTs(); + await().alias("wait for fresh interval") + .atMost(interval + 100, TimeUnit.MILLISECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .until(() -> customInterval.getCurrentIntervalStartTs() != initialIntervalStart); + long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs(); long tsBeforeInterval = currentIntervalStartTs - 1000; @@ -216,8 +225,6 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2)); postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3)); - long interval = customInterval.getCurrentIntervalDurationMillis(); - await().alias("create CF -> perform aggregation after interval end") .atMost(2 * interval, TimeUnit.MILLISECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeLatestVersionTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeLatestVersionTest.java new file mode 100644 index 0000000000..3852fe2ff0 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeLatestVersionTest.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.edge; + +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.edge.rpc.EdgeVersionComparator; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + +public class EdgeLatestVersionTest { + + @Test + public void edgeLatestVersionIsSynchronizedTest() { + EdgeVersion currentHighestEdgeVersion = EdgeVersionComparator.getNewestEdgeVersion(); + + String projectVersion = EdgeLatestVersionTest.class.getPackage().getImplementationVersion(); + if (projectVersion == null || projectVersion.isBlank()) { + projectVersion = System.getProperty("project.version", "UNKNOWN"); + } + + String projectVersionDigits = projectVersion.replaceAll("\\D", ""); + String currentHighestEdgeVersionDigits = currentHighestEdgeVersion.name().replaceAll("\\D", ""); + + String msg = "EdgeVersion enum in edge.proto is out of sync. Please add respective " + projectVersionDigits + " to EdgeVersion"; + Assert.assertEquals(msg, projectVersionDigits, currentHighestEdgeVersionDigits); + } + +} diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index d52074ace6..7eb601e270 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -106,6 +106,11 @@ com.google.protobuf protobuf-java + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index 1b17fbe849..7e58a2cb9f 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -143,16 +143,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { } public static EdgeVersion getNewestEdgeVersion() { - EdgeVersion newest = null; - for (EdgeVersion v : EdgeVersion.values()) { - if (v == EdgeVersion.V_LATEST || v == EdgeVersion.UNRECOGNIZED) { - continue; - } - if (newest == null || v.getNumber() > newest.getNumber()) { - newest = v; - } - } - return newest; + return EdgeVersionComparator.getNewestEdgeVersion(); } private StreamObserver initOutputStream(String edgeKey, diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeVersionComparator.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeVersionComparator.java new file mode 100644 index 0000000000..94ca577025 --- /dev/null +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeVersionComparator.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed 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.thingsboard.edge.rpc; + +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + +import java.util.Comparator; + +public class EdgeVersionComparator implements Comparator { + + public static final EdgeVersionComparator INSTANCE = new EdgeVersionComparator(); + + @Override + public int compare(EdgeVersion v1, EdgeVersion v2) { + if (v1 == v2) { + return 0; + } + // UNRECOGNIZED is less than any other version + if (v1 == EdgeVersion.UNRECOGNIZED) { + return -1; + } + if (v2 == EdgeVersion.UNRECOGNIZED) { + return 1; + } + // V_LATEST is treated as the newest version + if (v1 == EdgeVersion.V_LATEST) { + v1 = getNewestEdgeVersion(); + } + if (v2 == EdgeVersion.V_LATEST) { + v2 = getNewestEdgeVersion(); + } + return compareVersionParts(parseVersionParts(v1), parseVersionParts(v2)); + } + + public static EdgeVersion getNewestEdgeVersion() { + EdgeVersion newest = null; + for (EdgeVersion v : EdgeVersion.values()) { + if (v == EdgeVersion.V_LATEST || v == EdgeVersion.UNRECOGNIZED) { + continue; + } + if (newest == null || INSTANCE.compare(v, newest) > 0) { + newest = v; + } + } + return newest; + } + + private static int[] parseVersionParts(EdgeVersion version) { + String name = version.name(); + if (name.startsWith("V_")) { + name = name.substring(2); + } + String[] parts = name.split("_"); + int[] result = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + result[i] = Integer.parseInt(parts[i]); + } + return result; + } + + private static int compareVersionParts(int[] a, int[] b) { + int maxLen = Math.max(a.length, b.length); + for (int i = 0; i < maxLen; i++) { + int partA = i < a.length ? a[i] : 0; + int partB = i < b.length ? b[i] : 0; + if (partA != partB) { + return Integer.compare(partA, partB); + } + } + return 0; + } + +} diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index a01e0e561a..bbe871f55b 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -45,6 +45,8 @@ enum EdgeVersion { V_4_1_0 = 11; V_4_2_0 = 12; V_4_3_0 = 13; + V_4_2_1_2 = 14; + V_4_3_0_1 = 15; V_LATEST = 999; } diff --git a/common/edge-api/src/test/java/org/thingsboard/edge/rpc/EdgeVersionComparatorTest.java b/common/edge-api/src/test/java/org/thingsboard/edge/rpc/EdgeVersionComparatorTest.java new file mode 100644 index 0000000000..eab63fc092 --- /dev/null +++ b/common/edge-api/src/test/java/org/thingsboard/edge/rpc/EdgeVersionComparatorTest.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed 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.thingsboard.edge.rpc; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; + +import static org.assertj.core.api.Assertions.assertThat; + +class EdgeVersionComparatorTest { + + @Test + void compare_sameVersion_returnsZero() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_3_3_0)).isEqualTo(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_0_0, EdgeVersion.V_4_0_0)).isEqualTo(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_1_2, EdgeVersion.V_4_2_1_2)).isEqualTo(0); + } + + @Test + void compare_majorVersionDifference_returnsCorrectOrder() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_4_0_0)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_0_0, EdgeVersion.V_3_3_0)).isGreaterThan(0); + } + + @Test + void compare_minorVersionDifference_returnsCorrectOrder() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_3_6_0)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_0, EdgeVersion.V_3_3_0)).isGreaterThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_0_0, EdgeVersion.V_4_1_0)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_1_0, EdgeVersion.V_4_0_0)).isGreaterThan(0); + } + + @Test + void compare_patchVersionDifference_returnsCorrectOrder() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_0, EdgeVersion.V_3_6_1)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_1, EdgeVersion.V_3_6_0)).isGreaterThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_1, EdgeVersion.V_3_6_2)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_2, EdgeVersion.V_3_6_4)).isLessThan(0); + } + + @Test + void compare_fourPartVersion_returnsCorrectOrder() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_0, EdgeVersion.V_4_2_1_2)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_1_2, EdgeVersion.V_4_2_0)).isGreaterThan(0); + } + + @Test + void compare_threePartVsFourPart_treatsImplicitZero() { + // V_4_2_0 should be less than V_4_2_1_2 (4.2.0.0 < 4.2.1.2) + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_0, EdgeVersion.V_4_2_1_2)).isLessThan(0); + } + + @Test + void getNewestEdgeVersion_excludesLatestAndUnrecognized() { + EdgeVersion newest = EdgeVersionComparator.getNewestEdgeVersion(); + assertThat(newest).isNotNull(); + assertThat(newest).isNotEqualTo(EdgeVersion.V_LATEST); + assertThat(newest).isNotEqualTo(EdgeVersion.UNRECOGNIZED); + } + + @Test + void compare_vLatest_treatedAsNewestVersion() { + EdgeVersion newest = EdgeVersionComparator.getNewestEdgeVersion(); + // V_LATEST equals the newest version + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, newest)).isEqualTo(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(newest, EdgeVersion.V_LATEST)).isEqualTo(0); + // V_LATEST is greater than older versions + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, EdgeVersion.V_3_3_0)).isGreaterThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_LATEST)).isLessThan(0); + } + + @Test + void compare_vLatest_withItself_returnsZero() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, EdgeVersion.V_LATEST)).isEqualTo(0); + } + + @Test + void compare_unrecognized_isLessThanAnyVersion() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.UNRECOGNIZED, EdgeVersion.V_3_3_0)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.UNRECOGNIZED, EdgeVersion.V_LATEST)).isLessThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.UNRECOGNIZED)).isGreaterThan(0); + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, EdgeVersion.UNRECOGNIZED)).isGreaterThan(0); + } + + @Test + void compare_unrecognized_withItself_returnsZero() { + assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.UNRECOGNIZED, EdgeVersion.UNRECOGNIZED)).isEqualTo(0); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 4c5b228d3c..af4197d6ec 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -810,18 +810,27 @@ public class CalculatedFieldTest extends AbstractContainerTest { // --- Build CF: PROPAGATION --- CalculatedField saved = createPropagationCF(device.getId()); + // Wait for INITIALIZED event + waitForDebugEvent(saved, CalculatedFieldEventType.INITIALIZED.name(), 1); + // Create relations FROM asset 2 TO device Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null)); EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE); testRestClient.postEntityRelation(rel2); + // Wait for RELATION_ADD_OR_UPDATE event + waitForDebugEvent(saved, CalculatedFieldEventType.RELATION_ADD_OR_UPDATE.name(), 2); + // Telemetry on device testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25.1}")); + // Wait for POST_TELEMETRY_REQUEST event + waitForDebugEvent(saved, TbMsgType.POST_TELEMETRY_REQUEST.name(), 3); + // Delete relation between asset 1 and device testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId()); - // --- Assert propagated calculation (arguments-only mode) --- + // --- Assert all debug events in correct sequence --- await().alias("check debug events") .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -845,6 +854,19 @@ public class CalculatedFieldTest extends AbstractContainerTest { testRestClient.deleteAsset(asset2.getId()); } + private void waitForDebugEvent(CalculatedField cf, String expectedEventType, int expectedTotalEvents) { + await().alias("wait for debug event: " + expectedEventType) + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + List eventTypes = testRestClient.getEvents(cf.getId(), EventType.DEBUG_CALCULATED_FIELD, tenantId, new TimePageLink(expectedTotalEvents, 0, null, SortOrder.BY_CREATED_TIME_DESC)).getData().stream() + .map(e -> e.getBody().get("msgType").asText()) + .toList(); + assertThat(eventTypes).hasSize(expectedTotalEvents); + assertThat(eventTypes.get(0)).isEqualTo(expectedEventType); + }); + } + private CalculatedField createPropagationCF(EntityId entityId) { CalculatedField cf = new CalculatedField(); cf.setEntityId(entityId);