/** * Copyright © 2016-2022 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 math; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.math.TbMathArgument; import org.thingsboard.rule.engine.math.TbMathArgumentType; import org.thingsboard.rule.engine.math.TbMathNode; import org.thingsboard.rule.engine.math.TbMathNodeConfiguration; import org.thingsboard.rule.engine.math.TbMathResult; import org.thingsboard.rule.engine.math.TbRuleNodeMathFunctionType; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.Arrays; import java.util.Optional; import static org.mockito.Mockito.lenient; @RunWith(MockitoJUnitRunner.class) public class TbMathNodeTest { private EntityId originator = new DeviceId(Uuids.timeBased()); private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); @Mock private TbContext ctx; @Mock private AttributesService attributesService; @Mock private TimeseriesService tsService; private AbstractListeningExecutor dbExecutor; @Before public void before() { dbExecutor = new AbstractListeningExecutor() { @Override protected int getThreadPollSize() { return 3; } }; dbExecutor.init(); initMocks(); } @After public void after() { dbExecutor.destroy(); } private void initMocks() { Mockito.reset(ctx); Mockito.reset(attributesService); Mockito.reset(tsService); lenient().when(ctx.getAttributesService()).thenReturn(attributesService); lenient().when(ctx.getTimeseriesService()).thenReturn(tsService); lenient().when(ctx.getTenantId()).thenReturn(tenantId); lenient().when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); } private TbMathNode initNode(TbRuleNodeMathFunctionType operation, TbMathResult result, TbMathArgument... arguments) { return initNode(operation, null, result, arguments); } private TbMathNode initNodeWithCustomFunction(String expression, TbMathResult result, TbMathArgument... arguments) { return initNode(TbRuleNodeMathFunctionType.CUSTOM, expression, result, arguments); } private TbMathNode initNode(TbRuleNodeMathFunctionType operation, String expression, TbMathResult result, TbMathArgument... arguments) { try { TbMathNodeConfiguration configuration = new TbMathNodeConfiguration(); configuration.setOperation(operation); if (TbRuleNodeMathFunctionType.CUSTOM.equals(operation)) { configuration.setCustomFunction(expression); } configuration.setResult(result); configuration.setArguments(Arrays.asList(arguments)); TbMathNode node = new TbMathNode(); node.init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(configuration))); return node; } catch (TbNodeException ex) { throw new IllegalStateException(ex); } } @Test public void testExp4j() { var node = initNodeWithCustomFunction("2a+3b", new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); Assert.assertTrue(resultJson.has("result")); Assert.assertEquals(10, resultJson.get("result").asInt()); } @Test public void testSimpleFunctions() { testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.ADD, 2.1, 2.2, 4.3); testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.SUB, 2.1, 2.2, -0.1); testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MULT, 2.1, 2.0, 4.2); testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.DIV, 4.2, 2.0, 2.1); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIN, Math.toRadians(30), 0.5); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIN, Math.toRadians(90), 1.0); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SINH, Math.toRadians(0), 0.0); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COSH, Math.toRadians(0), 1.0); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COS, Math.toRadians(60), 0.5); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.COS, Math.toRadians(0), 1.0); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(45), 1); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(0), 0); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ABS, -1, 1); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SQRT, 4, 2); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.CBRT, 8, 2); } private void testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double arg2, double result) { initMocks(); var node = initNode(function, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).put("b", arg2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000).times(1)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); Assert.assertTrue(resultJson.has("result")); Assert.assertEquals(result, resultJson.get("result").asDouble(), 0d); } private void testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double result) { initMocks(); var node = initNode(function, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", arg1).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); Assert.assertTrue(resultJson.has("result")); Assert.assertEquals(result, resultJson.get("result").asDouble(), 0d); } @Test public void test_2_plus_2_body() { var node = initNode(TbRuleNodeMathFunctionType.ADD, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); Assert.assertTrue(resultJson.has("result")); Assert.assertEquals(4, resultJson.get("result").asInt()); } @Test public void test_2_plus_2_meta() { var node = initNode(TbRuleNodeMathFunctionType.ADD, new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 0, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a"), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "b") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 2).put("b", 2).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); Assert.assertNotNull(resultMsg.getMetaData()); var result = resultMsg.getMetaData().getValue("result"); Assert.assertNotNull(result); Assert.assertEquals("4", result); } @Test public void test_2_plus_2_attr_and_ts() { var node = initNode(TbRuleNodeMathFunctionType.ADD, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 2, false, false, null), new TbMathArgument(TbMathArgumentType.ATTRIBUTE, "a"), new TbMathArgument(TbMathArgumentType.TIME_SERIES, "b") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().toString()); Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, "a")) .thenReturn(Futures.immediateFuture(Optional.of(new BaseAttributeKvEntry(System.currentTimeMillis(), new DoubleDataEntry("a", 2.0))))); Mockito.when(tsService.findLatest(tenantId, originator, "b")) .thenReturn(Futures.immediateFuture(Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("b", 2L))))); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); Assert.assertTrue(resultJson.has("result")); Assert.assertEquals(4, resultJson.get("result").asInt()); } @Test public void test_sqrt_5_body() { var node = initNode(TbRuleNodeMathFunctionType.SQRT, new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 3, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); Assert.assertTrue(resultJson.has("result")); Assert.assertEquals(2.236, resultJson.get("result").asDouble(), 0.0); } @Test public void test_sqrt_5_meta() { var node = initNode(TbRuleNodeMathFunctionType.SQRT, new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 3, false, false, null), new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") ); TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); node.onMsg(ctx, msg); ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); TbMsg resultMsg = msgCaptor.getValue(); Assert.assertNotNull(resultMsg); Assert.assertNotNull(resultMsg.getData()); var result = resultMsg.getMetaData().getValue("result"); Assert.assertNotNull(result); Assert.assertEquals("2.236", result); } }