From 9e416e7dee40865dae6650d3738f846bfa19a56e Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 24 Nov 2025 19:31:07 +0200 Subject: [PATCH] bugfixes for propagation and geofencing CFs --- .../ctx/state/BaseCalculatedFieldState.java | 5 ++ .../cf/ctx/state/CalculatedFieldState.java | 16 +++++- .../GeofencingCalculatedFieldState.java | 26 ++++++++-- .../geofencing/GeofencingEvalResult.java | 2 +- .../state/geofencing/GeofencingZoneState.java | 10 ++-- .../cf/CalculatedFieldIntegrationTest.java | 21 +++----- .../GeofencingCalculatedFieldStateTest.java | 52 +++++++++++++------ .../cf/ctx/state/GeofencingZoneStateTest.java | 22 ++++---- .../PropagationCalculatedFieldStateTest.java | 33 ++++++++---- .../server/msa/cf/CalculatedFieldTest.java | 7 ++- 10 files changed, 124 insertions(+), 70 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 741c94c796..d6cf2bc845 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -25,6 +25,8 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.EntityAggregationArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import org.thingsboard.server.utils.CalculatedFieldUtils; import java.io.Closeable; @@ -164,6 +166,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, .mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : 0L) .max() .orElse(0L); + } else if (entry instanceof GeofencingArgumentEntry geofencingArgumentEntry) { + newTs = geofencingArgumentEntry.getZoneStates().values().stream() + .mapToLong(GeofencingZoneState::getTs).max().orElse(0L); } this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index e3914cc125..ec5681202f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -38,6 +38,7 @@ import java.io.Closeable; import java.util.List; import java.util.Map; +import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @@ -102,14 +103,25 @@ public interface CalculatedFieldState extends Closeable { record ReadinessStatus(boolean ready, String errorMsg) { - private static final String ERROR_MESSAGE = "Required arguments are missing: "; + private static final String MISSING_REQUIRED_ARGUMENTS_ERROR = "Required arguments are missing: "; + private static final String MISSING_PROPAGATION_TARGETS_ERROR = "No entities found via 'Propagation path to related entities'. " + + "Verify the relation type and direction configured."; + private static final String MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR = MISSING_PROPAGATION_TARGETS_ERROR + " Missing arguments to propagate: "; private static final ReadinessStatus READY = new ReadinessStatus(true, null); public static ReadinessStatus from(List emptyOrMissingArguments) { if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { return ReadinessStatus.READY; } - return new ReadinessStatus(false, ERROR_MESSAGE + String.join(", ", emptyOrMissingArguments)); + boolean propagationCtxIsEmpty = emptyOrMissingArguments.remove(PROPAGATION_CONFIG_ARGUMENT); + if (!propagationCtxIsEmpty) { + return new ReadinessStatus(false, MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments)); + } + if (emptyOrMissingArguments.isEmpty()) { + return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_ERROR); + } + return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR + + String.join(", ", emptyOrMissingArguments)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index b3ea94e62c..6d1e4e99ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -29,6 +29,7 @@ import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; @@ -112,7 +113,12 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { if (createRelationsWithMatchedZones) { GeofencingTransitionEvent transitionEvent = eval.transition(); if (transitionEvent == null) { - return; + if (!eval.firstEvaluation()) { + return; + } + transitionEvent = eval.status() == GeofencingPresenceStatus.INSIDE ? + GeofencingTransitionEvent.ENTERED : + GeofencingTransitionEvent.LEFT; } EntityRelation relation = switch (zoneGroupCfg.getDirection()) { case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType()); @@ -178,15 +184,27 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); - boolean prevInside = zoneResults.stream() - .anyMatch(r -> GeofencingTransitionEvent.LEFT.equals(r.transition()) || r.transition() == null && r.status() == INSIDE); + + boolean firstEvaluation = zoneResults.stream().allMatch(GeofencingEvalResult::firstEvaluation); + if (firstEvaluation) { + return new GeofencingEvalResult(null, nowInside ? INSIDE : OUTSIDE, true); + } + + boolean prevInside = zoneResults.stream().anyMatch(r -> { + if (r.firstEvaluation()) { + return false; + } + return GeofencingTransitionEvent.LEFT.equals(r.transition()) + || (r.transition() == null && r.status() == INSIDE); + }); + GeofencingTransitionEvent transition = null; if (!prevInside && nowInside) { transition = GeofencingTransitionEvent.ENTERED; } else if (prevInside && !nowInside) { transition = GeofencingTransitionEvent.LEFT; } - return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE); + return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE, false); } private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java index c6bf3dd65e..edc3f01ae7 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java @@ -20,5 +20,5 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.Geofencing import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, - GeofencingPresenceStatus status) { + GeofencingPresenceStatus status, boolean firstEvaluation) { } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java index c849f5d169..8a49461d31 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java @@ -86,21 +86,17 @@ public class GeofencingZoneState { // first evaluation if (this.lastPresence == null) { this.lastPresence = status; - GeofencingTransitionEvent transition = null; - if (status == GeofencingPresenceStatus.INSIDE) { - transition = GeofencingTransitionEvent.ENTERED; - } - return new GeofencingEvalResult(transition, status); + return new GeofencingEvalResult(null, status, true); } // State changed if (this.lastPresence != status) { this.lastPresence = status; GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ? GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT; - return new GeofencingEvalResult(transition, status); + return new GeofencingEvalResult(transition, status, false); } // State unchanged - return new GeofencingEvalResult(null, status); + return new GeofencingEvalResult(null, status, false); } } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index af0c1b9909..01dbf9ae2c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -714,7 +714,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/calculatedField", cf, CalculatedField.class); - // --- Assert initial evaluation (ENTERED / OUTSIDE) --- + // --- Assert initial evaluation (INSIDE / OUTSIDE) --- await().alias("initial geofencing evaluation") .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -722,10 +722,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent"); // --- no restrictedZonesEvent as no transition happened yet - assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") - .containsEntry("allowedZonesStatus", "INSIDE") + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") .containsEntry("restrictedZonesStatus", "OUTSIDE"); }); @@ -737,8 +736,6 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); // --- Assert no transition --- - // --- Assert attributes updated with the same values for restrictedZones --- - // --- Assert attributes updated with the new values for allowedZones --- await().alias("evaluation after version bump of geo argument") .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) @@ -824,17 +821,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes doPost("/api/calculatedField", cf, CalculatedField.class); - // --- Assert initial evaluation (ENTERED / OUTSIDE) --- + // --- Assert initial evaluation (INSIDE / OUTSIDE) --- await().alias("initial geofencing evaluation") .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") - .containsEntry("allowedZonesStatus", "INSIDE") + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") .containsEntry("restrictedZonesStatus", "OUTSIDE"); }); @@ -935,10 +931,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") - .containsEntry("allowedZonesStatus", "INSIDE"); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE"); }); // --- Move device OUTSIDE Zone A (expect LEFT) --- diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 24a0f0294a..ecba5acc77 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -221,7 +221,7 @@ public class GeofencingCalculatedFieldStateTest { ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, "allowedZones", geofencingAllowedZoneArgEntry, - "restrictedZones", new GeofencingArgumentEntry() + "restrictedZones", new GeofencingArgumentEntry(Collections.emptyMap()) ), ctx); assertThat(state.isReady()).isFalse(); assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); @@ -249,7 +249,6 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getResult()).isEqualTo( JacksonUtil.newObjectNode() - .put("allowedZonesEvent", "ENTERED") .put("allowedZonesStatus", "INSIDE") .put("restrictedZonesStatus", "OUTSIDE") ); @@ -290,10 +289,17 @@ public class GeofencingCalculatedFieldStateTest { assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); - verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); - EntityRelation leftRelation = deleteCaptor.getValue(); - assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); - assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + List deleteValues = deleteCaptor.getAllValues(); + assertThat(deleteValues).hasSize(2); + + EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0); + assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + + EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1); + assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); } @Test @@ -322,9 +328,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); - assertThat(result.getResult()).isEqualTo( - JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED") - ); + assertThat(result.getResult()).isEqualTo(JacksonUtil.newObjectNode()); SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); @@ -360,10 +364,17 @@ public class GeofencingCalculatedFieldStateTest { assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); - verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); - EntityRelation leftRelation = deleteCaptor.getValue(); - assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); - assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + List deleteValues = deleteCaptor.getAllValues(); + assertThat(deleteValues).hasSize(2); + + EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0); + assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + + EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1); + assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); } @Test @@ -432,10 +443,17 @@ public class GeofencingCalculatedFieldStateTest { assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); - verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); - EntityRelation leftRelation = deleteCaptor.getValue(); - assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); - assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + List deleteValues = deleteCaptor.getAllValues(); + assertThat(deleteValues).hasSize(2); + + EntityRelation deleteRelationFromFirstIteration = deleteValues.get(0); + assertThat(deleteRelationFromFirstIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(deleteRelationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + + EntityRelation deleteRelationFromSecondIteration = deleteValues.get(1); + assertThat(deleteRelationFromSecondIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(deleteRelationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); } private CalculatedField getCalculatedField() { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index f6c6778ced..7c2cfa56b3 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -48,30 +48,30 @@ public class GeofencingZoneStateTest { void evaluate_initialInside_thenInsideAgain() { var inside = new Coordinates(50.4730, 30.5050); // first evaluation: no prior state -> ENTERED - assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE, true)); // same position again -> INSIDE (steady state) - assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE, false)); } @Test void evaluate_initialOutside_thenOutsideAgain() { var outside = new Coordinates(50.4760, 30.5110); // first evaluation: no prior state -> OUTSIDE - assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE, true)); // same position again -> OUTSIDE (steady state) - assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE, false)); } @Test void evaluate_inside_thenLeave() { var inside = new Coordinates(50.4730, 30.5050); var outside = new Coordinates(50.4760, 30.5110); - // enter - assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + // inside + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE, true)); // leave -> LEFT - assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE)); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE, false)); // still outside -> OUTSIDE - assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE, false)); } @Test @@ -79,11 +79,11 @@ public class GeofencingZoneStateTest { var outside = new Coordinates(50.4760, 30.5110); var inside = new Coordinates(50.4730, 30.5050); // start outside - assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE, true)); // cross boundary -> ENTERED - assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE, false)); // remain inside -> INSIDE - assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE, false)); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index 6ef945e4c6..e5bd6720e0 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -52,10 +54,12 @@ import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalcul import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -126,21 +130,28 @@ public class PropagationCalculatedFieldStateTest { assertThat(state.isReady()).isFalse(); } - @Test - void testIsReadyWhenPropagationArgIsNull() { - initCtxAndState(false); - state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); - assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); + private static Stream provideInvalidPropagationArgs() { + return Stream.of( + null, + new PropagationArgumentEntry(Collections.emptyList()) + ); } - @Test - void testIsReadyWhenPropagationArgIsEmpty() { + @ParameterizedTest + @MethodSource("provideInvalidPropagationArgs") + void testIsReadyWhenPropagationArgIsNullOrEmpty(ArgumentEntry propagationEntry) { initCtxAndState(false); - state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, - PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); + + Map args = new HashMap<>(); + args.put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); // Valid user arg + + if (propagationEntry != null) { + args.put(PROPAGATION_CONFIG_ARGUMENT, propagationEntry); + } + state.update(args, ctx); assertThat(state.isReady()).isFalse(); - assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.getReadinessStatus().errorMsg()) + .isEqualTo("No entities found via 'Propagation path to related entities'. Verify the relation type and direction configured."); } @Test 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 43d9a159fa..43fcbda5af 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 @@ -409,11 +409,10 @@ public class CalculatedFieldTest extends AbstractContainerTest { .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, - "allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus"); - assertThat(attrs).isNotNull().hasSize(3); + "allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus"); + assertThat(attrs).isNotNull().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") - .containsEntry("allowedZonesStatus", "INSIDE") + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") .containsEntry("restrictedZonesStatus", "OUTSIDE"); });