Browse Source

bugfixes for propagation and geofencing CFs

pull/14414/head
dshvaika 10 months ago
parent
commit
9e416e7dee
  1. 5
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java
  2. 16
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java
  3. 26
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java
  5. 10
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java
  6. 21
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  7. 52
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java
  8. 22
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java
  9. 33
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java
  10. 7
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

5
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.CalculatedFieldEntityCtxId;
import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesArgumentEntry; 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.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 org.thingsboard.server.utils.CalculatedFieldUtils;
import java.io.Closeable; import java.io.Closeable;
@ -164,6 +166,9 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState,
.mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : 0L) .mapToLong(e -> (e instanceof SingleValueArgumentEntry s) ? s.getTs() : 0L)
.max() .max()
.orElse(0L); .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); this.latestTimestamp = Math.max(this.latestTimestamp, newTs);
} }

16
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.List;
import java.util.Map; 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; import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@ -102,14 +103,25 @@ public interface CalculatedFieldState extends Closeable {
record ReadinessStatus(boolean ready, String errorMsg) { 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); private static final ReadinessStatus READY = new ReadinessStatus(true, null);
public static ReadinessStatus from(List<String> emptyOrMissingArguments) { public static ReadinessStatus from(List<String> emptyOrMissingArguments) {
if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) {
return ReadinessStatus.READY; 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));
} }
} }

26
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.CalculatedFieldType;
import org.thingsboard.server.common.data.cf.configuration.OutputType; 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.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.GeofencingReportStrategy;
import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
@ -112,7 +113,12 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
if (createRelationsWithMatchedZones) { if (createRelationsWithMatchedZones) {
GeofencingTransitionEvent transitionEvent = eval.transition(); GeofencingTransitionEvent transitionEvent = eval.transition();
if (transitionEvent == null) { if (transitionEvent == null) {
return; if (!eval.firstEvaluation()) {
return;
}
transitionEvent = eval.status() == GeofencingPresenceStatus.INSIDE ?
GeofencingTransitionEvent.ENTERED :
GeofencingTransitionEvent.LEFT;
} }
EntityRelation relation = switch (zoneGroupCfg.getDirection()) { EntityRelation relation = switch (zoneGroupCfg.getDirection()) {
case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType()); case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType());
@ -178,15 +184,27 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) { private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); 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; GeofencingTransitionEvent transition = null;
if (!prevInside && nowInside) { if (!prevInside && nowInside) {
transition = GeofencingTransitionEvent.ENTERED; transition = GeofencingTransitionEvent.ENTERED;
} else if (prevInside && !nowInside) { } else if (prevInside && !nowInside) {
transition = GeofencingTransitionEvent.LEFT; 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) { private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) {

2
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; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent;
public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition,
GeofencingPresenceStatus status) { GeofencingPresenceStatus status, boolean firstEvaluation) {
} }

10
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java

@ -86,21 +86,17 @@ public class GeofencingZoneState {
// first evaluation // first evaluation
if (this.lastPresence == null) { if (this.lastPresence == null) {
this.lastPresence = status; this.lastPresence = status;
GeofencingTransitionEvent transition = null; return new GeofencingEvalResult(null, status, true);
if (status == GeofencingPresenceStatus.INSIDE) {
transition = GeofencingTransitionEvent.ENTERED;
}
return new GeofencingEvalResult(transition, status);
} }
// State changed // State changed
if (this.lastPresence != status) { if (this.lastPresence != status) {
this.lastPresence = status; this.lastPresence = status;
GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ? GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ?
GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT; GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT;
return new GeofencingEvalResult(transition, status); return new GeofencingEvalResult(transition, status, false);
} }
// State unchanged // State unchanged
return new GeofencingEvalResult(null, status); return new GeofencingEvalResult(null, status, false);
} }
} }

21
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); doPost("/api/calculatedField", cf, CalculatedField.class);
// --- Assert initial evaluation (ENTERED / OUTSIDE) --- // --- Assert initial evaluation (INSIDE / OUTSIDE) ---
await().alias("initial geofencing evaluation") await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -722,10 +722,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
ArrayNode attrs = getServerAttributes(device.getId(), ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent"); "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent");
// --- no restrictedZonesEvent as no transition happened yet // --- no restrictedZonesEvent as no transition happened yet
assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs); Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE"); .containsEntry("restrictedZonesStatus", "OUTSIDE");
}); });
@ -737,8 +736,6 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk());
// --- Assert no transition --- // --- 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") await().alias("evaluation after version bump of geo argument")
.atMost(TIMEOUT, TimeUnit.SECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -824,17 +821,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
doPost("/api/calculatedField", cf, CalculatedField.class); doPost("/api/calculatedField", cf, CalculatedField.class);
// --- Assert initial evaluation (ENTERED / OUTSIDE) --- // --- Assert initial evaluation (INSIDE / OUTSIDE) ---
await().alias("initial geofencing evaluation") await().alias("initial geofencing evaluation")
.atMost(TIMEOUT, TimeUnit.SECONDS) .atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(), ArrayNode attrs = getServerAttributes(device.getId(),
"allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus"); "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); assertThat(attrs).isNotNull().isNotEmpty().hasSize(2);
Map<String, String> m = kv(attrs); Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE"); .containsEntry("restrictedZonesStatus", "OUTSIDE");
}); });
@ -935,10 +931,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus");
assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); assertThat(attrs).isNotNull().isNotEmpty().hasSize(1);
Map<String, String> m = kv(attrs); Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") assertThat(m).containsEntry("allowedZonesStatus", "INSIDE");
.containsEntry("allowedZonesStatus", "INSIDE");
}); });
// --- Move device OUTSIDE Zone A (expect LEFT) --- // --- Move device OUTSIDE Zone A (expect LEFT) ---

52
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_LATITUDE_ARGUMENT_KEY, latitudeArgEntry,
ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry,
"allowedZones", geofencingAllowedZoneArgEntry, "allowedZones", geofencingAllowedZoneArgEntry,
"restrictedZones", new GeofencingArgumentEntry() "restrictedZones", new GeofencingArgumentEntry(Collections.emptyMap())
), ctx); ), ctx);
assertThat(state.isReady()).isFalse(); assertThat(state.isReady()).isFalse();
assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones"); assertThat(state.getReadinessStatus().errorMsg()).contains("restrictedZones");
@ -249,7 +249,6 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo( assertThat(result.getResult()).isEqualTo(
JacksonUtil.newObjectNode() JacksonUtil.newObjectNode()
.put("allowedZonesEvent", "ENTERED")
.put("allowedZonesStatus", "INSIDE") .put("allowedZonesStatus", "INSIDE")
.put("restrictedZonesStatus", "OUTSIDE") .put("restrictedZonesStatus", "OUTSIDE")
); );
@ -290,10 +289,17 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone");
ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
EntityRelation leftRelation = deleteCaptor.getValue(); List<EntityRelation> deleteValues = deleteCaptor.getAllValues();
assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); assertThat(deleteValues).hasSize(2);
assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
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 @Test
@ -322,9 +328,7 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(result).isNotNull(); assertThat(result).isNotNull();
assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo( assertThat(result.getResult()).isEqualTo(JacksonUtil.newObjectNode());
JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED")
);
SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L);
SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L);
@ -360,10 +364,17 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone");
ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
EntityRelation leftRelation = deleteCaptor.getValue(); List<EntityRelation> deleteValues = deleteCaptor.getAllValues();
assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); assertThat(deleteValues).hasSize(2);
assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
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 @Test
@ -432,10 +443,17 @@ public class GeofencingCalculatedFieldStateTest {
assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone"); assertThat(relationFromSecondIteration.getType()).isEqualTo("CurrentZone");
ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); ArgumentCaptor<EntityRelation> deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class);
verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); verify(relationService, times(2)).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture());
EntityRelation leftRelation = deleteCaptor.getValue(); List<EntityRelation> deleteValues = deleteCaptor.getAllValues();
assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); assertThat(deleteValues).hasSize(2);
assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId());
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() { private CalculatedField getCalculatedField() {

22
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java

@ -48,30 +48,30 @@ public class GeofencingZoneStateTest {
void evaluate_initialInside_thenInsideAgain() { void evaluate_initialInside_thenInsideAgain() {
var inside = new Coordinates(50.4730, 30.5050); var inside = new Coordinates(50.4730, 30.5050);
// first evaluation: no prior state -> ENTERED // 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) // 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 @Test
void evaluate_initialOutside_thenOutsideAgain() { void evaluate_initialOutside_thenOutsideAgain() {
var outside = new Coordinates(50.4760, 30.5110); var outside = new Coordinates(50.4760, 30.5110);
// first evaluation: no prior state -> OUTSIDE // 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) // 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 @Test
void evaluate_inside_thenLeave() { void evaluate_inside_thenLeave() {
var inside = new Coordinates(50.4730, 30.5050); var inside = new Coordinates(50.4730, 30.5050);
var outside = new Coordinates(50.4760, 30.5110); var outside = new Coordinates(50.4760, 30.5110);
// enter // inside
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE, true));
// leave -> LEFT // leave -> LEFT
assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE)); assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE, false));
// still outside -> OUTSIDE // still outside -> OUTSIDE
assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE, false));
} }
@Test @Test
@ -79,11 +79,11 @@ public class GeofencingZoneStateTest {
var outside = new Coordinates(50.4760, 30.5110); var outside = new Coordinates(50.4760, 30.5110);
var inside = new Coordinates(50.4730, 30.5050); var inside = new Coordinates(50.4730, 30.5050);
// start outside // start outside
assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE, true));
// cross boundary -> ENTERED // 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 // remain inside -> INSIDE
assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE, false));
} }
@Test @Test

33
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 io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -126,21 +130,28 @@ public class PropagationCalculatedFieldStateTest {
assertThat(state.isReady()).isFalse(); assertThat(state.isReady()).isFalse();
} }
@Test private static Stream<ArgumentEntry> provideInvalidPropagationArgs() {
void testIsReadyWhenPropagationArgIsNull() { return Stream.of(
initCtxAndState(false); null,
state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry), ctx); new PropagationArgumentEntry(Collections.emptyList())
assertThat(state.isReady()).isFalse(); );
assertThat(state.getReadinessStatus().errorMsg()).contains(PROPAGATION_CONFIG_ARGUMENT);
} }
@Test @ParameterizedTest
void testIsReadyWhenPropagationArgIsEmpty() { @MethodSource("provideInvalidPropagationArgs")
void testIsReadyWhenPropagationArgIsNullOrEmpty(ArgumentEntry propagationEntry) {
initCtxAndState(false); initCtxAndState(false);
state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry,
PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())), ctx); Map<String, ArgumentEntry> 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.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 @Test

7
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) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE, ArrayNode attrs = testRestClient.getAttributes(device.getId(), SERVER_SCOPE,
"allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus"); "allowedZonesEvent,allowedZonesStatus,restrictedZonesEvent,restrictedZonesStatus");
assertThat(attrs).isNotNull().hasSize(3); assertThat(attrs).isNotNull().hasSize(2);
Map<String, String> m = kv(attrs); Map<String, String> m = kv(attrs);
assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") assertThat(m).containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("allowedZonesStatus", "INSIDE")
.containsEntry("restrictedZonesStatus", "OUTSIDE"); .containsEntry("restrictedZonesStatus", "OUTSIDE");
}); });

Loading…
Cancel
Save