Add tests for compilation errors when evaluating scripts

This commit is contained in:
Dmytro Skarzhynets 2025-07-08 15:32:24 +03:00
parent 2055dc83be
commit a0e8b01429
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
4 changed files with 131 additions and 7 deletions

View File

@ -25,11 +25,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.script.api.js.NashornJsInvokeService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -39,6 +41,7 @@ import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST;
@ -59,6 +62,25 @@ class NashornJsInvokeServiceTest extends AbstractControllerTest {
@Value("${js.local.max_errors}")
private int maxJsErrors;
@Test
void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() {
// GIVEN
var uncompilableScript = "return msg.temperature?.value;";
// WHEN-THEN
assertThatThrownBy(() -> evalScript(uncompilableScript))
.isInstanceOf(ExecutionException.class)
.cause()
.isInstanceOf(TbScriptException.class)
.asInstanceOf(type(TbScriptException.class))
.satisfies(ex -> {
assertThat(ex.getScriptId()).isNotNull();
assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION);
assertThat(ex.getBody()).contains(uncompilableScript);
assertThat(ex.getCause()).isInstanceOf(ScriptException.class);
});
}
@Test
void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException {
int iterations = 1000;

View File

@ -23,9 +23,9 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.stats.DefaultStatsFactory;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
@ -42,8 +42,11 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doAnswer;
@ -60,7 +63,6 @@ class RemoteJsInvokeServiceTest {
private RemoteJsInvokeService remoteJsInvokeService;
private TbQueueRequestTemplate<TbProtoJsQueueMsg<RemoteJsRequest>, TbProtoQueueMsg<RemoteJsResponse>> jsRequestTemplate;
@BeforeEach
public void beforeEach() {
TbApiUsageStateClient apiUsageStateClient = mock(TbApiUsageStateClient.class);
@ -74,7 +76,7 @@ class RemoteJsInvokeServiceTest {
remoteJsInvokeService.requestTemplate = jsRequestTemplate;
StatsFactory statsFactory = mock(StatsFactory.class);
when(statsFactory.createStatsCounter(any(), any())).thenReturn(mock(StatsCounter.class));
ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory",statsFactory);
ReflectionTestUtils.setField(remoteJsInvokeService, "statsFactory", statsFactory);
remoteJsInvokeService.init();
}
@ -84,7 +86,36 @@ class RemoteJsInvokeServiceTest {
}
@Test
public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception {
void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() {
// GIVEN
doAnswer(methodCall -> Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
.setCompileResponse(JsInvokeProtos.JsCompileResponse.newBuilder()
.setSuccess(false)
.setErrorCode(JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR)
.setErrorDetails("SyntaxError: Unexpected token 'const'")
.setScriptHash(methodCall.<TbProtoQueueMsg<RemoteJsRequest>>getArgument(0).getValue().getCompileRequest().getScriptHash())
.build())
.build())))
.when(jsRequestTemplate).send(argThat(jsQueueMsg -> jsQueueMsg.getValue().hasCompileRequest()));
var uncompilableScript = "let const = 'this is not allowed';";
// WHEN-THEN
assertThatThrownBy(() -> remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, uncompilableScript).get())
.isInstanceOf(ExecutionException.class)
.cause()
.isInstanceOf(TbScriptException.class)
.asInstanceOf(type(TbScriptException.class))
.satisfies(ex -> {
assertThat(ex.getScriptId()).isNotNull();
assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION);
assertThat(ex.getBody()).contains(uncompilableScript);
assertThat(ex.getCause()).isInstanceOf(RuntimeException.class).hasMessage("SyntaxError: Unexpected token 'const'");
});
}
@Test
void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception {
mockJsEvalResponse();
String scriptBody = "return { a: 'b'};";
UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
@ -110,7 +141,7 @@ class RemoteJsInvokeServiceTest {
}
@Test
public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception {
void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception {
mockJsEvalResponse();
String scriptBody = "return { a: 'b'};";
UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();
@ -156,7 +187,7 @@ class RemoteJsInvokeServiceTest {
}
@Test
public void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception {
void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception {
mockJsEvalResponse();
TenantId tenantId1 = TenantId.fromUUID(UUID.randomUUID());
@ -187,7 +218,7 @@ class RemoteJsInvokeServiceTest {
}
@Test
public void whenReleasingScript_thenCheckForHashUsages() throws Exception {
void whenReleasingScript_thenCheckForHashUsages() throws Exception {
mockJsEvalResponse();
String scriptBody = "return { a: 'b'};";
UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptBody).get();

View File

@ -20,10 +20,12 @@ import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.mvel2.CompileException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.TbScriptException;
import org.thingsboard.script.api.tbel.TbelScript;
import java.io.Serializable;
@ -37,6 +39,7 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
@TestPropertySource(properties = {
"tbel.max_script_body_size=100",
@ -50,6 +53,25 @@ class TbelInvokeServiceTest extends AbstractTbelInvokeTest {
@Value("${tbel.max_errors}")
private int maxJsErrors;
@Test
void givenUncompilableScript_whenEvaluating_thenThrowsErrorWithCompilationErrorCode() {
// GIVEN
var uncompilableScript = "return msg.property !== undefined;";
// WHEN-THEN
assertThatThrownBy(() -> evalScript(uncompilableScript))
.isInstanceOf(ExecutionException.class)
.cause()
.isInstanceOf(TbScriptException.class)
.asInstanceOf(type(TbScriptException.class))
.satisfies(ex -> {
assertThat(ex.getScriptId()).isNotNull();
assertThat(ex.getErrorCode()).isEqualTo(TbScriptException.ErrorCode.COMPILATION);
assertThat(ex.getBody()).isEqualTo(uncompilableScript);
assertThat(ex.getCause()).isInstanceOf(CompileException.class);
});
}
@Test
void givenSimpleScriptTestPerformance() throws ExecutionException, InterruptedException {
int iterations = 100000;

View File

@ -0,0 +1,49 @@
/**
* Copyright © 2016-2025 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 org.thingsboard.script.api;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.assertj.core.api.Assertions.assertThat;
class TbScriptExceptionTest {
@Test
void givenCompilationError_whenCheckingIsUnrecoverable_thenReturnsTrue() {
// GIVEN
var exception = new TbScriptException(null, TbScriptException.ErrorCode.COMPILATION, null, null);
// WHEN-THEN
assertThat(exception.isUnrecoverable()).isTrue();
}
@ParameterizedTest
@EnumSource(
value = TbScriptException.ErrorCode.class,
mode = EnumSource.Mode.EXCLUDE,
names = "COMPILATION"
)
void givenRecoverableErrorCodes_whenCheckingIsUnrecoverable_thenReturnsFalse(TbScriptException.ErrorCode errorCode) {
// GIVEN
var exception = new TbScriptException(null, errorCode, null, null);
// WHEN-THEN
assertThat(exception.isUnrecoverable()).isFalse();
}
}