Added missing tests for new class + refactoring of geofencing calculation logic
This commit is contained in:
parent
dd53892df2
commit
87a27e95ef
@ -44,7 +44,6 @@ import org.thingsboard.script.api.tbel.TbelInvokeService;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.EventInfo;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
||||
import org.thingsboard.server.common.data.event.EventType;
|
||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||
@ -279,17 +278,15 @@ public class CalculatedFieldController extends BaseController {
|
||||
}
|
||||
|
||||
private void checkReferencedEntities(CalculatedFieldConfiguration calculatedFieldConfig) throws ThingsboardException {
|
||||
if (calculatedFieldConfig instanceof ArgumentsBasedCalculatedFieldConfiguration config) {
|
||||
List<EntityId> referencedEntityIds = config.getReferencedEntities();
|
||||
for (EntityId referencedEntityId : referencedEntityIds) {
|
||||
EntityType entityType = referencedEntityId.getEntityType();
|
||||
switch (entityType) {
|
||||
case TENANT -> {
|
||||
return;
|
||||
}
|
||||
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
|
||||
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
|
||||
List<EntityId> referencedEntityIds = calculatedFieldConfig.getReferencedEntities();
|
||||
for (EntityId referencedEntityId : referencedEntityIds) {
|
||||
EntityType entityType = referencedEntityId.getEntityType();
|
||||
switch (entityType) {
|
||||
case TENANT -> {
|
||||
return;
|
||||
}
|
||||
case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ);
|
||||
default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.common.util.JacksonUtil;
|
||||
import org.thingsboard.common.util.geo.Coordinates;
|
||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||
@ -39,12 +40,13 @@ import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
|
||||
@ -116,18 +118,8 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue();
|
||||
Coordinates entityCoordinates = new Coordinates(latitude, longitude);
|
||||
|
||||
var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
|
||||
// TODO: refactor
|
||||
return calculate(entityId, ctx, entityCoordinates, configuration);
|
||||
}
|
||||
|
||||
private ListenableFuture<CalculatedFieldResult> calculate(
|
||||
EntityId entityId,
|
||||
CalculatedFieldCtx ctx,
|
||||
Coordinates entityCoordinates,
|
||||
GeofencingCalculatedFieldConfiguration configuration) {
|
||||
|
||||
Map<String, ZoneGroupConfiguration> zoneGroups = configuration
|
||||
var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration();
|
||||
Map<String, ZoneGroupConfiguration> zoneGroups = geofencingCfg
|
||||
.getZoneGroups()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ZoneGroupConfiguration::getName, Function.identity()));
|
||||
@ -136,41 +128,40 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
List<ListenableFuture<Boolean>> relationFutures = new ArrayList<>();
|
||||
|
||||
getGeofencingArguments().forEach((argumentKey, argumentEntry) -> {
|
||||
ZoneGroupConfiguration zoneGroupConfiguration = zoneGroups.get(argumentKey);
|
||||
if (zoneGroupConfiguration.isCreateRelationsWithMatchedZones()) {
|
||||
List<GeofencingEvalResult> zoneResults = new ArrayList<>();
|
||||
|
||||
argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> {
|
||||
GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates);
|
||||
zoneResults.add(eval);
|
||||
|
||||
ZoneGroupConfiguration zoneGroupCfg = zoneGroups.get(argumentKey);
|
||||
if (zoneGroupCfg == null) {
|
||||
log.error("[{}][{}] Zone group config is missing for the {}", entityId, ctx.getCalculatedField().getId(), argumentKey);
|
||||
return;
|
||||
}
|
||||
boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones();
|
||||
List<GeofencingEvalResult> zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size());
|
||||
argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> {
|
||||
GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates);
|
||||
zoneResults.add(eval);
|
||||
if (createRelationsWithMatchedZones) {
|
||||
GeofencingTransitionEvent transitionEvent = eval.transition();
|
||||
if (transitionEvent == null) {
|
||||
return;
|
||||
}
|
||||
EntityRelation relation = toRelation(zoneId, entityId, zoneGroupConfiguration);
|
||||
EntityRelation relation = switch (zoneGroupCfg.getDirection()) {
|
||||
case TO -> new EntityRelation(zoneId, entityId, zoneGroupCfg.getRelationType());
|
||||
case FROM -> new EntityRelation(entityId, zoneId, zoneGroupCfg.getRelationType());
|
||||
};
|
||||
ListenableFuture<Boolean> f = switch (transitionEvent) {
|
||||
case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation);
|
||||
case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation);
|
||||
};
|
||||
relationFutures.add(f);
|
||||
});
|
||||
updateResultNode(argumentKey, zoneResults, zoneGroupConfiguration.getReportStrategy(), resultNode);
|
||||
} else {
|
||||
List<GeofencingEvalResult> zoneResults = argumentEntry.getZoneStates().values().stream()
|
||||
.map(zs -> zs.evaluate(entityCoordinates))
|
||||
.toList();
|
||||
updateResultNode(argumentKey, zoneResults, zoneGroupConfiguration.getReportStrategy(), resultNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode);
|
||||
});
|
||||
|
||||
var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode);
|
||||
if (relationFutures.isEmpty()) {
|
||||
return Futures.immediateFuture(result);
|
||||
}
|
||||
return Futures.whenAllComplete(relationFutures)
|
||||
.call(() -> new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode),
|
||||
MoreExecutors.directExecutor());
|
||||
return Futures.whenAllComplete(relationFutures).call(() -> result, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private Map<String, GeofencingArgumentEntry> getGeofencingArguments() {
|
||||
@ -194,12 +185,6 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
}
|
||||
}
|
||||
|
||||
private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) {
|
||||
if (aggregationResult.transition() != null) {
|
||||
resultNode.put(eventKey, aggregationResult.transition().name());
|
||||
}
|
||||
}
|
||||
|
||||
private GeofencingEvalResult aggregateZoneGroup(List<GeofencingEvalResult> zoneResults) {
|
||||
boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status()));
|
||||
boolean prevInside = zoneResults.stream()
|
||||
@ -213,11 +198,10 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState {
|
||||
return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE);
|
||||
}
|
||||
|
||||
private EntityRelation toRelation(EntityId zoneId, EntityId entityId, ZoneGroupConfiguration configuration) {
|
||||
return switch (configuration.getDirection()) {
|
||||
case TO -> new EntityRelation(zoneId, entityId, configuration.getRelationType());
|
||||
case FROM -> new EntityRelation(entityId, zoneId, configuration.getRelationType());
|
||||
};
|
||||
private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) {
|
||||
if (aggregationResult.transition() != null) {
|
||||
resultNode.put(eventKey, aggregationResult.transition().name());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,7 +15,119 @@
|
||||
*/
|
||||
package org.thingsboard.server.common.data.cf.configuration.geofencing;
|
||||
|
||||
// TODO: add tests
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.thingsboard.server.common.data.AttributeScope;
|
||||
import org.thingsboard.server.common.data.cf.configuration.Argument;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ArgumentType;
|
||||
import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey;
|
||||
import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration;
|
||||
import org.thingsboard.server.common.data.relation.EntityRelation;
|
||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS;
|
||||
|
||||
public class ZoneGroupConfigurationTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = " ")
|
||||
@NullAndEmptySource
|
||||
void validateShouldThrowWhenNameIsNullEmptyOrBlank(String name) {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration(name, "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
assertThatThrownBy(zoneGroupConfiguration::validate)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Zone group name must be specified!");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY, EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY})
|
||||
void validateShouldThrowWhenUsedReservedEntityCoordinateNames(String name) {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration(name, "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
assertThatThrownBy(zoneGroupConfiguration::validate)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Name '" + name + "' is reserved and cannot be used for zone group!");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = " ")
|
||||
@NullAndEmptySource
|
||||
void validateShouldThrowWhenPerimeterKeyNameIsNullEmptyOrBlank(String perimeterKeyName) {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", perimeterKeyName, REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
assertThatThrownBy(zoneGroupConfiguration::validate)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Perimeter key name must be specified for 'allowedZonesGroup' zone group!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateShouldThrowWhenReportStrategyIsNull() {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", null, false);
|
||||
assertThatThrownBy(zoneGroupConfiguration::validate)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Report strategy must be specified for 'allowedZonesGroup' zone group!");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = " ")
|
||||
@NullAndEmptySource
|
||||
void validateShouldThrowWhenRelationCreationEnabledAndRelationTypeIsNullEmptyOrBlank(String relationType) {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true);
|
||||
zoneGroupConfiguration.setRelationType(relationType);
|
||||
assertThatThrownBy(zoneGroupConfiguration::validate)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Relation type must be specified for 'allowedZonesGroup' zone group!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateShouldThrowWhenRelationCreationEnabledAndDirectionIsNull() {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true);
|
||||
zoneGroupConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE);
|
||||
zoneGroupConfiguration.setDirection(null);
|
||||
assertThatThrownBy(zoneGroupConfiguration::validate)
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Relation direction must be specified for 'allowedZonesGroup' zone group!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateShouldDoesNotThrowAnyExceptionWhenRelationCreationDisabledAndConfigValid() {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
assertThatCode(zoneGroupConfiguration::validate).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateShouldDoesNotThrowAnyExceptionWhenRelationCreationEnabledAndConfigValid() {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, true);
|
||||
zoneGroupConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE);
|
||||
zoneGroupConfiguration.setDirection(EntitySearchDirection.TO);
|
||||
assertThatCode(zoneGroupConfiguration::validate).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNotNull() {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration());
|
||||
assertThat(zoneGroupConfiguration.hasDynamicSource()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNull() {
|
||||
var zoneGroupConfiguration = mock(ZoneGroupConfiguration.class);
|
||||
assertThat(zoneGroupConfiguration.getRefDynamicSourceConfiguration()).isNull();
|
||||
assertThat(zoneGroupConfiguration.hasDynamicSource()).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void validateToArgumentsMethodCallWithoutRefEntityId() {
|
||||
var zoneGroupConfiguration = new ZoneGroupConfiguration("allowedZonesGroup", "perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
|
||||
Argument zoneGroupArgument = zoneGroupConfiguration.toArgument();
|
||||
assertThat(zoneGroupArgument).isNotNull();
|
||||
assertThat(zoneGroupArgument.getRefEntityKey()).isEqualTo(new ReferencedEntityKey("perimeter", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user