@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel ;
import org.thingsboard.server.common.stats.DefaultStatsFactory ;
import org.thingsboard.server.dao.usagerecord.ApiLimitService ;
import org.thingsboard.server.service.cf.CalculatedFieldProcessingService ;
import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult ;
import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult ;
import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry ;
@ -57,12 +58,17 @@ import java.util.Collections;
import java.util.HashMap ;
import java.util.List ;
import java.util.Map ;
import java.util.Optional ;
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 ;
import static org.mockito.ArgumentMatchers.anyLong ;
import static org.mockito.Mockito.never ;
import static org.mockito.Mockito.spy ;
import static org.mockito.Mockito.verify ;
import static org.mockito.Mockito.when ;
import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT ;
@ -70,17 +76,26 @@ import static org.thingsboard.server.common.data.cf.configuration.PropagationCal
public class PropagationCalculatedFieldStateTest {
private static final String TEMPERATURE_ARGUMENT_NAME = "t" ;
private static final String HUMIDITY_ARGUMENT_NAME = "h" ;
private static final String TEST_RESULT_EXPRESSION_KEY = "testResult" ;
private static final double TEMPERATURE_VALUE = 12 . 5 ;
private static final double HUMIDITY_VALUE = 85 ;
private static final PropagationArgumentEntry EMPTY_PROPAGATION_ARGUMENT = new PropagationArgumentEntry ( Collections . emptyList ( ) ) ;
private final TenantId TENANT_ID = TenantId . fromUUID ( UUID . fromString ( "6c3513cb-85e7-4510-8746-1ba01859a8ce" ) ) ;
private final DeviceId DEVICE_ID = new DeviceId ( UUID . fromString ( "be960a50-c029-4698-b2ec-c56a543c561c" ) ) ;
private final AssetId ASSET_ID_1 = new AssetId ( UUID . fromString ( "d26f0e5b-7d7d-4a61-9f5e-08ab97b30734" ) ) ;
private final AssetId ASSET_ID_2 = new AssetId ( UUID . fromString ( "1933a317-4df5-4d36-9800-68aded74579b" ) ) ;
private final SingleValueArgumentEntry singleValueArgEntry =
private final SingleValueArgumentEntry EMPTY_SINGLE_VALUE_ARGUMENT = new SingleValueArgumentEntry ( ) ;
private final SingleValueArgumentEntry temperatureArgumentEntry =
new SingleValueArgumentEntry ( System . currentTimeMillis ( ) , new DoubleDataEntry ( "temperature" , TEMPERATURE_VALUE ) , 99L ) ;
private final SingleValueArgumentEntry humidityArgumentEntry =
new SingleValueArgumentEntry ( System . currentTimeMillis ( ) , new DoubleDataEntry ( "humidity" , HUMIDITY_VALUE ) , 99L ) ;
private final PropagationArgumentEntry propagationArgEntry =
new PropagationArgumentEntry ( new ArrayList < > ( List . of ( ASSET_ID_2 , ASSET_ID_1 ) ) ) ;
@ -96,15 +111,19 @@ public class PropagationCalculatedFieldStateTest {
@MockitoBean
private ActorSystemContext actorSystemContext ;
@MockitoBean
private CalculatedFieldProcessingService cfProcessingService ;
@BeforeEach
void setUp ( ) {
when ( actorSystemContext . getTbelInvokeService ( ) ) . thenReturn ( tbelInvokeService ) ;
when ( actorSystemContext . getApiLimitService ( ) ) . thenReturn ( apiLimitService ) ;
when ( actorSystemContext . getCalculatedFieldProcessingService ( ) ) . thenReturn ( cfProcessingService ) ;
when ( apiLimitService . getLimit ( any ( ) , any ( ) ) ) . thenReturn ( 1000L ) ;
}
void initCtxAndState ( boolean applyExpressionToResolvedArguments ) {
ctx = new CalculatedFieldCtx ( getCalculatedField ( applyExpressionToResolvedArguments ) , actorSystemContext ) ;
ctx = spy ( new CalculatedFieldCtx ( getCalculatedField ( applyExpressionToResolvedArguments ) , actorSystemContext ) ) ;
ctx . init ( ) ;
state = new PropagationCalculatedFieldState ( ctx . getEntityId ( ) ) ;
@ -121,7 +140,7 @@ public class PropagationCalculatedFieldStateTest {
@Test
void testInitAddsRequiredArgument ( ) {
initCtxAndState ( false ) ;
assertThat ( state . getRequiredArguments ( ) ) . containsExactlyInAnyOrder ( TEMPERATURE_ARGUMENT_NAME , PROPAGATION_CONFIG_ARGUMENT ) ;
assertThat ( state . getRequiredArguments ( ) ) . containsExactlyInAnyOrder ( TEMPERATURE_ARGUMENT_NAME , HUMIDITY_ARGUMENT_NAME , PROPAGATION_CONFIG_ARGUMENT ) ;
}
@Test
@ -133,7 +152,7 @@ public class PropagationCalculatedFieldStateTest {
private static Stream < ArgumentEntry > provideInvalidPropagationArgs ( ) {
return Stream . of (
null ,
new PropagationArgumentEntry ( Collections . emptyList ( ) )
EMPTY_PROPAGATION_ARGUMENT
) ;
}
@ -143,7 +162,8 @@ public class PropagationCalculatedFieldStateTest {
initCtxAndState ( false ) ;
Map < String , ArgumentEntry > args = new HashMap < > ( ) ;
args . put ( TEMPERATURE_ARGUMENT_NAME , singleValueArgEntry ) ; // Valid user arg
args . put ( TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ) ; // Valid user arg
args . put ( HUMIDITY_ARGUMENT_NAME , humidityArgumentEntry ) ; // Valid user arg
if ( propagationEntry ! = null ) {
args . put ( PROPAGATION_CONFIG_ARGUMENT , propagationEntry ) ;
@ -155,19 +175,54 @@ public class PropagationCalculatedFieldStateTest {
}
@Test
void testIsReadyWhenPropagationArgHasEntities ( ) {
void testIsReadyWithoutExpressionWhenAllArgumentsAreNotEmpty ( ) {
initCtxAndState ( false ) ;
state . update ( Map . of ( TEMPERATURE_ARGUMENT_NAME , singleValueArgEntry , PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) , ctx ) ;
Map < String , ArgumentEntry > updatedArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , humidityArgumentEntry ,
PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry
) ;
state . update ( updatedArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isTrue ( ) ;
assertThat ( state . getReadinessStatus ( ) . errorMsg ( ) ) . isNull ( ) ;
}
@Test
void testIsReadyWithoutExpressionWhenAtLeastOneArgumentIsNotEmpty ( ) {
initCtxAndState ( false ) ;
Map < String , ArgumentEntry > updatedArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , EMPTY_SINGLE_VALUE_ARGUMENT ,
PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) ;
state . update ( updatedArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isTrue ( ) ;
assertThat ( state . getReadinessStatus ( ) . errorMsg ( ) ) . isNull ( ) ;
}
@Test
void testIsNotReadyWithExpressionWhenAtLeastOneArgumentIsEmpty ( ) {
initCtxAndState ( true ) ;
Map < String , ArgumentEntry > updatedArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , EMPTY_SINGLE_VALUE_ARGUMENT ,
PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) ;
state . update ( updatedArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isFalse ( ) ;
assertThat ( state . getReadinessStatus ( ) . errorMsg ( ) ) . isEqualTo ( "Required arguments are missing: h" ) ;
}
@Test
void testPerformCalculationWithEmptyPropagationArg ( ) throws Exception {
initCtxAndState ( false ) ;
state . getArguments ( ) . put ( PROPAGATION_CONFIG_ARGUMENT , new PropagationArgumentEntry ( Collections . emptyList ( ) ) ) ;
Map < String , ArgumentEntry > initArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , humidityArgumentEntry ,
PROPAGATION_CONFIG_ARGUMENT , EMPTY_PROPAGATION_ARGUMENT ) ;
state . update ( initArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isFalse ( ) ;
// test empty propagation argument calculation
PropagationCalculatedFieldResult result = performCalculation ( ) ;
assertThat ( result ) . isNotNull ( ) ;
@ -178,8 +233,12 @@ public class PropagationCalculatedFieldStateTest {
@Test
void testPerformCalculationWithArgumentsOnlyMode ( ) throws Exception {
initCtxAndState ( false ) ;
state . getArguments ( ) . put ( PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) ;
state . getArguments ( ) . put ( TEMPERATURE_ARGUMENT_NAME , singleValueArgEntry ) ;
Map < String , ArgumentEntry > initArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , EMPTY_SINGLE_VALUE_ARGUMENT ,
PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) ;
state . update ( initArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isTrue ( ) ;
PropagationCalculatedFieldResult propagationResult = performCalculation ( ) ;
@ -193,7 +252,7 @@ public class PropagationCalculatedFieldStateTest {
assertThat ( result . getScope ( ) ) . isEqualTo ( AttributeScope . SERVER_SCOPE ) ;
ObjectNode expectedNode = JacksonUtil . newObjectNode ( ) ;
JacksonUtil . addKvEntry ( expectedNode , singleValueArg Entry. getKvEntryValue ( ) , TEMPERATURE_ARGUMENT_NAME ) ;
JacksonUtil . addKvEntry ( expectedNode , temperatureArgument Entry. getKvEntryValue ( ) , TEMPERATURE_ARGUMENT_NAME ) ;
assertThat ( result . getResult ( ) ) . isEqualTo ( expectedNode ) ;
}
@ -201,9 +260,13 @@ public class PropagationCalculatedFieldStateTest {
@Test
void testPerformCalculationWithExpressionResultMode ( ) throws Exception {
initCtxAndState ( true ) ;
state . getArguments ( ) . put ( PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) ;
state . getArguments ( ) . put ( TEMPERATURE_ARGUMENT_NAME , singleValueArgEntry ) ;
Map < String , ArgumentEntry > initArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , humidityArgumentEntry ,
PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry
) ;
state . update ( initArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isTrue ( ) ;
PropagationCalculatedFieldResult propagationResult = performCalculation ( ) ;
assertThat ( propagationResult ) . isNotNull ( ) ;
@ -216,7 +279,7 @@ public class PropagationCalculatedFieldStateTest {
assertThat ( result . getScope ( ) ) . isEqualTo ( AttributeScope . SERVER_SCOPE ) ;
ObjectNode expectedNode = JacksonUtil . newObjectNode ( ) ;
expectedNode . put ( TEST_RESULT_EXPRESSION_KEY , TEMPERATURE_VALUE * 2 ) ;
expectedNode . put ( TEST_RESULT_EXPRESSION_KEY , ( TEMPERATURE_VALUE + HUMIDITY_VALUE ) / 2 ) ;
assertThat ( result . getResult ( ) ) . isEqualTo ( expectedNode ) ;
}
@ -224,18 +287,52 @@ public class PropagationCalculatedFieldStateTest {
@Test
void testPropagationWithUpdatedPropagationArgument ( ) throws ExecutionException , InterruptedException {
initCtxAndState ( false ) ;
state . getArguments ( ) . put ( PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry ) ;
state . getArguments ( ) . put ( TEMPERATURE_ARGUMENT_NAME , singleValueArgEntry ) ;
Map < String , ArgumentEntry > initArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , EMPTY_SINGLE_VALUE_ARGUMENT ,
PROPAGATION_CONFIG_ARGUMENT , propagationArgEntry
) ;
state . update ( initArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isTrue ( ) ;
AssetId newEntityId = new AssetId ( UUID . fromString ( "83e2c962-eeae-4708-984e-e6a24760f9c3" ) ) ;
PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry ( ) ;
propagationArgumentEntry . setAdded ( newEntityId ) ;
propagationArgumentEntry . setAdded ( List . of ( newEntityId ) ) ;
Map < String , ArgumentEntry > updated = state . update ( Map . of ( PROPAGATION_CONFIG_ARGUMENT , propagationArgumentEntry ) , ctx ) ;
assertThat ( updated ) . isNotNull ( ) . containsEntry ( PROPAGATION_CONFIG_ARGUMENT , propagationArgumentEntry ) ;
PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation ( updated ) ;
assertThat ( propagationCalculatedFieldResult ) . isNotNull ( ) ;
assertThat ( propagationCalculatedFieldResult . getEntityIds ( ) ) . isNotNull ( ) . containsExactly ( newEntityId ) ;
assertThat ( propagationCalculatedFieldResult . getResult ( ) ) . isNotNull ( ) ;
assertThat ( propagationCalculatedFieldResult . getResult ( ) . getResult ( ) ) . isNotNull ( ) ;
assertThat ( propagationCalculatedFieldResult . getResult ( ) . getResult ( ) ) . isEqualTo ( JacksonUtil . newObjectNode ( ) . put ( TEMPERATURE_ARGUMENT_NAME , TEMPERATURE_VALUE ) ) ;
}
@Test
void testPropapagationStateInitWithRestoredSetToFalse ( ) {
initCtxAndState ( false ) ;
verify ( cfProcessingService , never ( ) ) . fetchPropagationArgumentFromDb ( any ( ) , any ( ) ) ;
verify ( ctx , never ( ) ) . scheduleReevaluation ( anyLong ( ) , any ( ) ) ;
}
@Test
void testPropapagationStateInitWithRestoredSetToTrue ( ) {
initCtxAndState ( false ) ;
Map < String , ArgumentEntry > initArgs = Map . of (
TEMPERATURE_ARGUMENT_NAME , temperatureArgumentEntry ,
HUMIDITY_ARGUMENT_NAME , humidityArgumentEntry ,
PROPAGATION_CONFIG_ARGUMENT , new PropagationArgumentEntry ( Collections . emptyList ( ) )
) ;
state . update ( initArgs , ctx ) ;
assertThat ( state . isReady ( ) ) . isFalse ( ) ;
when ( cfProcessingService . fetchPropagationArgumentFromDb ( any ( ) , any ( ) ) ) . thenReturn ( Optional . of ( propagationArgEntry ) ) ;
state . init ( true ) ;
verify ( cfProcessingService ) . fetchPropagationArgumentFromDb ( ctx , state . getEntityId ( ) ) ;
verify ( ctx ) . scheduleReevaluation ( 0L , state . getActorCtx ( ) ) ;
}
private CalculatedField getCalculatedField ( boolean applyExpressionToResolvedArguments ) {
@ -260,8 +357,12 @@ public class PropagationCalculatedFieldStateTest {
ReferencedEntityKey tempKey = new ReferencedEntityKey ( "temperature" , ArgumentType . TS_LATEST , null ) ;
temperatureArg . setRefEntityKey ( tempKey ) ;
config . setArguments ( Map . of ( TEMPERATURE_ARGUMENT_NAME , temperatureArg ) ) ;
config . setExpression ( "{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}" ) ;
Argument humidityArg = new Argument ( ) ;
ReferencedEntityKey humidityKey = new ReferencedEntityKey ( "humidity" , ArgumentType . TS_LATEST , null ) ;
humidityArg . setRefEntityKey ( humidityKey ) ;
config . setArguments ( Map . of ( TEMPERATURE_ARGUMENT_NAME , temperatureArg , HUMIDITY_ARGUMENT_NAME , humidityArg ) ) ;
config . setExpression ( "return { " + TEST_RESULT_EXPRESSION_KEY + ": (" + TEMPERATURE_ARGUMENT_NAME + " + " + HUMIDITY_ARGUMENT_NAME + ") / 2};" ) ;
AttributesOutput output = new AttributesOutput ( ) ;
output . setScope ( AttributeScope . SERVER_SCOPE ) ;