This is an automated email from the ASF dual-hosted git repository.
wangjian pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new bd5afab901 test: add unit tests for saga-engine low coverage
components (#7915)
bd5afab901 is described below
commit bd5afab9013e589752a9b85ed7d9ac77a6efda9f
Author: Eric Wang <[email protected]>
AuthorDate: Wed Mar 4 15:01:26 2026 +0800
test: add unit tests for saga-engine low coverage components (#7915)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 1 +
.../exception/EngineExecutionExceptionTest.java | 111 ++++++++
.../exception/ForwardInvalidExceptionTest.java | 89 ++++++
.../exception/ExceptionMatchExpressionTest.java | 118 ++++++++
.../impl/ProcessCtrlStateMachineEngineTest.java | 148 ++++++++++
.../pcext/handlers/LoopStartStateHandlerTest.java | 240 ++++++++++++++++
.../pcext/handlers/SubStateMachineHandlerTest.java | 312 +++++++++++++++++++++
.../LoopTaskHandlerInterceptorTest.java | 261 +++++++++++++++++
.../engine/pcext/utils/LoopContextHolderTest.java | 141 ++++++++++
.../saga/engine/pcext/utils/LoopTaskUtilsTest.java | 298 ++++++++++++++++++++
.../repo/impl/StateLogRepositoryImplTest.java | 132 +++++++++
.../saga/engine/utils/ExceptionUtilsTest.java | 144 ++++++++++
13 files changed, 1996 insertions(+)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 192d97de1e..b55f16e1cf 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -52,6 +52,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#7962](https://github.com/apache/incubator-seata/pull/7962)] add unit
tests for NacosRegistryProvider and NacosRegistryServiceImpl
- [[#8003](https://github.com/apache/incubator-seata/pull/8003)] Enhance
NacosRegistryServiceImplTest with additional mocks for service name group and
cluster
+- [[#7915](https://github.com/apache/incubator-seata/pull/7915)] add unit
tests for saga-engine low coverage components
### refactor:
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 3da705d6b3..cea60b183a 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -57,6 +57,7 @@
- [[#7962](https://github.com/apache/incubator-seata/pull/7962)] 为
NacosRegistryProvider 和 NacosRegistryServiceImpl 添加单元测试用例
- [[#8003](https://github.com/apache/incubator-seata/pull/8003)] 为
NacosRegistryServiceImplTest 增加服务名称、分组和集群的额外模拟
+- [[#7915](https://github.com/apache/incubator-seata/pull/7915)] 为 saga-engine
添加单元测试用例
### refactor:
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/EngineExecutionExceptionTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/EngineExecutionExceptionTest.java
new file mode 100644
index 0000000000..6b6471afb0
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/EngineExecutionExceptionTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.exception;
+
+import org.apache.seata.common.exception.FrameworkErrorCode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Test class for {@link EngineExecutionException}
+ */
+public class EngineExecutionExceptionTest {
+
+ @Test
+ public void defaultConstructorTest() {
+ EngineExecutionException e = new EngineExecutionException();
+ assertNotNull(e);
+ // Parent class FrameworkException default constructor sets the
default message
+ assertNotNull(e.getMessage());
+ }
+
+ @Test
+ public void constructorWithErrorCodeTest() {
+ EngineExecutionException e = new
EngineExecutionException(FrameworkErrorCode.UnknownAppError);
+ assertNotNull(e);
+ assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+ }
+
+ @Test
+ public void constructorWithMessageTest() {
+ EngineExecutionException e = new EngineExecutionException("Test error
message");
+ assertEquals("Test error message", e.getMessage());
+ }
+
+ @Test
+ public void constructorWithMessageAndErrorCodeTest() {
+ EngineExecutionException e = new EngineExecutionException("Test
error", FrameworkErrorCode.UnknownAppError);
+ assertEquals("Test error", e.getMessage());
+ assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+ }
+
+ @Test
+ public void constructorWithCauseTest() {
+ Throwable cause = new RuntimeException("root cause");
+ EngineExecutionException e = new EngineExecutionException(cause);
+ assertSame(cause, e.getCause());
+ }
+
+ @Test
+ public void constructorWithCauseAndMessageTest() {
+ Throwable cause = new RuntimeException("root cause");
+ EngineExecutionException e = new EngineExecutionException(cause,
"wrapper message");
+ assertEquals("wrapper message", e.getMessage());
+ assertSame(cause, e.getCause());
+ }
+
+ @Test
+ public void constructorWithCauseMessageAndErrorCodeTest() {
+ Throwable cause = new RuntimeException("root cause");
+ EngineExecutionException e =
+ new EngineExecutionException(cause, "wrapper message",
FrameworkErrorCode.UnknownAppError);
+ assertEquals("wrapper message", e.getMessage());
+ assertSame(cause, e.getCause());
+ assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+ }
+
+ @Test
+ public void stateNameGetterSetterTest() {
+ EngineExecutionException e = new EngineExecutionException();
+ e.setStateName("TestState");
+ assertEquals("TestState", e.getStateName());
+ }
+
+ @Test
+ public void stateMachineNameGetterSetterTest() {
+ EngineExecutionException e = new EngineExecutionException();
+ e.setStateMachineName("TestMachine");
+ assertEquals("TestMachine", e.getStateMachineName());
+ }
+
+ @Test
+ public void stateMachineInstanceIdGetterSetterTest() {
+ EngineExecutionException e = new EngineExecutionException();
+ e.setStateMachineInstanceId("instance-123");
+ assertEquals("instance-123", e.getStateMachineInstanceId());
+ }
+
+ @Test
+ public void stateInstanceIdGetterSetterTest() {
+ EngineExecutionException e = new EngineExecutionException();
+ e.setStateInstanceId("state-456");
+ assertEquals("state-456", e.getStateInstanceId());
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/ForwardInvalidExceptionTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/ForwardInvalidExceptionTest.java
new file mode 100644
index 0000000000..824f5bca5e
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/exception/ForwardInvalidExceptionTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.exception;
+
+import org.apache.seata.common.exception.FrameworkErrorCode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * Test class for {@link ForwardInvalidException}
+ */
+public class ForwardInvalidExceptionTest {
+
+ @Test
+ public void defaultConstructorTest() {
+ ForwardInvalidException e = new ForwardInvalidException();
+ assertNotNull(e);
+ }
+
+ @Test
+ public void constructorWithErrorCodeTest() {
+ ForwardInvalidException e = new
ForwardInvalidException(FrameworkErrorCode.UnknownAppError);
+ assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+ }
+
+ @Test
+ public void constructorWithMessageTest() {
+ ForwardInvalidException e = new ForwardInvalidException("forward
invalid");
+ assertEquals("forward invalid", e.getMessage());
+ }
+
+ @Test
+ public void constructorWithMessageAndErrorCodeTest() {
+ ForwardInvalidException e = new ForwardInvalidException("forward
invalid", FrameworkErrorCode.UnknownAppError);
+ assertEquals("forward invalid", e.getMessage());
+ assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+ }
+
+ @Test
+ public void constructorWithCauseMessageAndErrorCodeTest() {
+ Throwable cause = new RuntimeException("root");
+ ForwardInvalidException e =
+ new ForwardInvalidException(cause, "forward invalid",
FrameworkErrorCode.UnknownAppError);
+ assertSame(cause, e.getCause());
+ assertEquals("forward invalid", e.getMessage());
+ assertEquals(FrameworkErrorCode.UnknownAppError, e.getErrcode());
+ }
+
+ @Test
+ public void constructorWithCauseTest() {
+ Throwable cause = new RuntimeException("root");
+ ForwardInvalidException e = new ForwardInvalidException(cause);
+ assertSame(cause, e.getCause());
+ }
+
+ @Test
+ public void constructorWithCauseAndMessageTest() {
+ Throwable cause = new RuntimeException("root");
+ ForwardInvalidException e = new ForwardInvalidException(cause,
"forward invalid");
+ assertEquals("forward invalid", e.getMessage());
+ assertSame(cause, e.getCause());
+ }
+
+ @Test
+ public void inheritanceFromEngineExecutionExceptionTest() {
+ ForwardInvalidException e = new ForwardInvalidException("test");
+ e.setStateName("TestState");
+ e.setStateMachineName("TestMachine");
+ assertEquals("TestState", e.getStateName());
+ assertEquals("TestMachine", e.getStateMachineName());
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/expression/exception/ExceptionMatchExpressionTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/expression/exception/ExceptionMatchExpressionTest.java
new file mode 100644
index 0000000000..71279637c9
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/expression/exception/ExceptionMatchExpressionTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.expression.exception;
+
+import org.apache.seata.saga.engine.exception.EngineExecutionException;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test class for {@link ExceptionMatchExpression}
+ */
+public class ExceptionMatchExpressionTest {
+
+ @Test
+ public void getValueWhenExactClassNameMatchReturnTrueTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.lang.RuntimeException");
+ Object result = expr.getValue(new RuntimeException("test"));
+ assertEquals(true, result);
+ }
+
+ @Test
+ public void getValueWhenSubclassMatchReturnTrueTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.lang.Exception");
+ Object result = expr.getValue(new RuntimeException("test"));
+ assertEquals(true, result);
+ }
+
+ @Test
+ public void getValueWhenNoMatchReturnFalseTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.io.IOException");
+ Object result = expr.getValue(new RuntimeException("test"));
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void getValueWhenContextIsNotExceptionReturnFalseTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.lang.RuntimeException");
+ Object result = expr.getValue("not an exception");
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void getValueWhenExpressionStringEmptyReturnFalseTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ // expressionString is null by default
+ Object result = expr.getValue(new RuntimeException());
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void getValueWhenContextIsNullReturnFalseTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.lang.RuntimeException");
+ Object result = expr.getValue(null);
+ assertEquals(false, result);
+ }
+
+ @Test
+ public void setExpressionStringWithValidExceptionClassSetClassTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.lang.RuntimeException");
+ assertEquals("java.lang.RuntimeException", expr.getExpressionString());
+ }
+
+ @Test
+ public void setExpressionStringWithIOExceptionClassTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ expr.setExpressionString("java.io.IOException");
+ assertEquals("java.io.IOException", expr.getExpressionString());
+
+ // Test matching
+ Object result = expr.getValue(new IOException("test"));
+ assertEquals(true, result);
+ }
+
+ @Test
+ public void
setExpressionStringWithInvalidClassThrowEngineExecutionExceptionTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ assertThrows(EngineExecutionException.class, () ->
expr.setExpressionString("com.nonexistent.NotAnException"));
+ }
+
+ @Test
+ public void setValueDoNothingTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ // setValue is a no-op, just verify it doesn't throw
+ assertDoesNotThrow(() -> expr.setValue("value", "context"));
+ }
+
+ @Test
+ public void getExpressionStringWhenNotSetReturnNullTest() {
+ ExceptionMatchExpression expr = new ExceptionMatchExpression();
+ assertNull(expr.getExpressionString());
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/impl/ProcessCtrlStateMachineEngineTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/impl/ProcessCtrlStateMachineEngineTest.java
new file mode 100644
index 0000000000..ed2b2438be
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/impl/ProcessCtrlStateMachineEngineTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.impl;
+
+import org.apache.seata.saga.engine.StateMachineConfig;
+import org.apache.seata.saga.engine.exception.ForwardInvalidException;
+import org.apache.seata.saga.engine.store.StateLogStore;
+import org.apache.seata.saga.statelang.domain.ExecutionStatus;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.StateType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link ProcessCtrlStateMachineEngine}
+ */
+public class ProcessCtrlStateMachineEngineTest {
+
+ private ProcessCtrlStateMachineEngine engine;
+ private StateMachineConfig config;
+
+ @BeforeEach
+ public void setUp() {
+ engine = new ProcessCtrlStateMachineEngine();
+ config = mock(StateMachineConfig.class);
+ engine.setStateMachineConfig(config);
+ }
+
+ @Test
+ public void getStateMachineConfigTest() {
+ assertSame(config, engine.getStateMachineConfig());
+ }
+
+ @Test
+ public void setStateMachineConfigTest() {
+ StateMachineConfig newConfig = mock(StateMachineConfig.class);
+ engine.setStateMachineConfig(newConfig);
+ assertSame(newConfig, engine.getStateMachineConfig());
+ }
+
+ @Test
+ public void findOutLastForwardStateInstanceWithEmptyListReturnNullTest() {
+ List<StateInstance> emptyList = Collections.emptyList();
+ StateInstance result =
engine.findOutLastForwardStateInstance(emptyList);
+ assertNull(result);
+ }
+
+ @Test
+ public void findOutLastForwardStateInstanceWithCompensationStateSkipTest()
{
+ StateInstance stateInstance = mock(StateInstance.class);
+ when(stateInstance.isForCompensation()).thenReturn(true);
+
+ List<StateInstance> stateList = new ArrayList<>();
+ stateList.add(stateInstance);
+
+ StateInstance result =
engine.findOutLastForwardStateInstance(stateList);
+ assertNull(result);
+ }
+
+ @Test
+ public void
findOutLastForwardStateInstanceWithSuccessfulCompensationSkipTest() {
+ StateInstance stateInstance = mock(StateInstance.class);
+ when(stateInstance.isForCompensation()).thenReturn(false);
+
when(stateInstance.getCompensationStatus()).thenReturn(ExecutionStatus.SU);
+
+ List<StateInstance> stateList = new ArrayList<>();
+ stateList.add(stateInstance);
+
+ StateInstance result =
engine.findOutLastForwardStateInstance(stateList);
+ assertNull(result);
+ }
+
+ @Test
+ public void
findOutLastForwardStateInstanceReturnLastNonCompensationStateTest() {
+ StateInstance state1 = mock(StateInstance.class);
+ StateInstance state2 = mock(StateInstance.class);
+
+ when(state1.isForCompensation()).thenReturn(false);
+ when(state1.getCompensationStatus()).thenReturn(null);
+ when(state1.getType()).thenReturn(StateType.SERVICE_TASK);
+
+ when(state2.isForCompensation()).thenReturn(false);
+ when(state2.getCompensationStatus()).thenReturn(null);
+ when(state2.getType()).thenReturn(StateType.SERVICE_TASK);
+
+ List<StateInstance> stateList = new ArrayList<>();
+ stateList.add(state1);
+ stateList.add(state2);
+
+ StateInstance result =
engine.findOutLastForwardStateInstance(stateList);
+ assertSame(state2, result);
+ }
+
+ @Test
+ public void
findOutLastForwardStateInstanceWithUNCompensationStatusThrowExceptionTest() {
+ StateInstance stateInstance = mock(StateInstance.class);
+ when(stateInstance.isForCompensation()).thenReturn(false);
+
when(stateInstance.getCompensationStatus()).thenReturn(ExecutionStatus.UN);
+ when(stateInstance.getType()).thenReturn(StateType.SERVICE_TASK);
+ when(stateInstance.getId()).thenReturn("state-123");
+
+ List<StateInstance> stateList = new ArrayList<>();
+ stateList.add(stateInstance);
+
+ assertThrows(ForwardInvalidException.class, () ->
engine.findOutLastForwardStateInstance(stateList));
+ }
+
+ @Test
+ public void reloadStateMachineInstanceWhenNullReturnNullTest() {
+ StateLogStore stateLogStore = mock(StateLogStore.class);
+ when(config.getStateLogStore()).thenReturn(stateLogStore);
+
when(stateLogStore.getStateMachineInstance("non-existent")).thenReturn(null);
+
+ StateMachineInstance result =
engine.reloadStateMachineInstance("non-existent");
+ assertNull(result);
+ }
+
+ @Test
+ public void engineNotNullTest() {
+ assertNotNull(engine);
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/LoopStartStateHandlerTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/LoopStartStateHandlerTest.java
new file mode 100644
index 0000000000..d1cffed1c1
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/LoopStartStateHandlerTest.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.pcext.handlers;
+
+import org.apache.seata.saga.engine.StateMachineConfig;
+import org.apache.seata.saga.engine.expression.Expression;
+import org.apache.seata.saga.engine.expression.ExpressionResolver;
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.engine.pcext.utils.LoopContextHolder;
+import org.apache.seata.saga.proctrl.HierarchicalProcessContext;
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.proctrl.eventing.impl.ProcessCtrlEventPublisher;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.StateType;
+import org.apache.seata.saga.statelang.domain.impl.AbstractTaskState;
+import org.apache.seata.saga.statelang.domain.impl.AbstractTaskState.LoopImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Semaphore;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link LoopStartStateHandler}
+ */
+public class LoopStartStateHandlerTest {
+
+ private LoopStartStateHandler handler;
+
+ @BeforeEach
+ public void setUp() {
+ handler = new LoopStartStateHandler();
+ }
+
+ @Test
+ public void processWhenLoopConfigNullSetTemporaryStateTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachineConfig config = mock(StateMachineConfig.class);
+ AbstractTaskState state = mock(AbstractTaskState.class);
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(state);
+ when(instruction.getStateName()).thenReturn("testState");
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
+ when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+ when(state.getLoop()).thenReturn(null);
+
+ handler.process(context);
+
+ verify(instruction).setTemporaryState(null);
+ verify(instruction).setTemporaryState(state);
+ }
+
+ @Test
+ public void processHandlerNotNullTest() {
+ assertNotNull(handler);
+ }
+
+ @Test
+ public void processCleanupContextVariablesInFinallyBlockTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachineConfig config = mock(StateMachineConfig.class);
+ AbstractTaskState state = mock(AbstractTaskState.class);
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(state);
+ when(instruction.getStateName()).thenReturn("testState");
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+ when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+ when(state.getLoop()).thenReturn(null);
+
+ handler.process(context);
+
+ verify(context).removeVariable(DomainConstants.LOOP_SEMAPHORE);
+ verify(context).removeVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE);
+
verify(context).removeVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER);
+ }
+
+ @Test
+ public void processWhenLoopContextHolderIsNullNotThrowExceptionTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachineConfig config = mock(StateMachineConfig.class);
+ AbstractTaskState state = mock(AbstractTaskState.class);
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(state);
+ when(instruction.getStateName()).thenReturn("testState");
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(null);
+ when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+ when(state.getLoop()).thenReturn(null);
+
+ assertDoesNotThrow(() -> handler.process(context));
+ }
+
+ @Test
+ public void processWhenLoopContextHolderExistsWithFailEndFalseTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachineConfig config = mock(StateMachineConfig.class);
+ AbstractTaskState state = mock(AbstractTaskState.class);
+
+ LoopContextHolder holder = new LoopContextHolder();
+ holder.setFailEnd(false);
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(state);
+ when(instruction.getStateName()).thenReturn("testState");
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+ when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+ when(state.getLoop()).thenReturn(null);
+
+ handler.process(context);
+
+ assertFalse(holder.isFailEnd());
+ }
+
+ @Test
+ public void processAsyncLoopExecutionHappyPathTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachineConfig config = mock(StateMachineConfig.class);
+ AbstractTaskState state = mock(AbstractTaskState.class);
+ ProcessCtrlEventPublisher publisher =
mock(ProcessCtrlEventPublisher.class);
+ ExpressionResolver resolver = mock(ExpressionResolver.class);
+
+ LoopImpl loop = new LoopImpl();
+ loop.setParallel(2);
+ loop.setCollection("$.users");
+ loop.setElementVariableName("user");
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(state);
+ when(instruction.getStateName()).thenReturn("loopState");
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(smInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONTEXT)).thenReturn(new
HashMap<>());
+
+ LoopContextHolder holder = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
+ when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+ when(state.getLoop()).thenReturn(loop);
+
+ Expression expression = mock(Expression.class);
+ List<String> userList = Arrays.asList("user1", "user2", "user3");
+ when(expression.getValue(any())).thenReturn(userList);
+ when(resolver.getExpression(any())).thenReturn(expression);
+ when(config.getExpressionResolver()).thenReturn(resolver);
+
+ when(config.isEnableAsync()).thenReturn(true);
+ when(config.getAsyncProcessCtrlEventPublisher()).thenReturn(publisher);
+
+ final Semaphore[] capturedSemaphore = new Semaphore[1];
+ doAnswer(invocation -> {
+ String key = invocation.getArgument(0);
+ Object value = invocation.getArgument(1);
+ if (DomainConstants.LOOP_SEMAPHORE.equals(key)) {
+ capturedSemaphore[0] = (Semaphore) value;
+ }
+ return null;
+ })
+ .when(context)
+ .setVariable(eq(DomainConstants.LOOP_SEMAPHORE), any());
+
+ // Merged doAnswer handling both StateInstance injection AND Semaphore
release
+ doAnswer(invocation -> {
+ ProcessContext ctx = invocation.getArgument(0);
+
+ StateInstance mockStateInst = mock(StateInstance.class);
+ when(mockStateInst.getOutputParams()).thenReturn(new
HashMap<>());
+
+ // Crucial Fix: Use setVariableLocally to prevent
delegation to mocked parent
+ if (ctx instanceof HierarchicalProcessContext) {
+ ((HierarchicalProcessContext) ctx)
+
.setVariableLocally(DomainConstants.VAR_NAME_STATE_INST, mockStateInst);
+ } else {
+ ctx.setVariable(DomainConstants.VAR_NAME_STATE_INST,
mockStateInst);
+ }
+
+ if (capturedSemaphore[0] != null) {
+ capturedSemaphore[0].release();
+ }
+ return null;
+ })
+ .when(publisher)
+ .publish(any());
+
+ handler.process(context);
+
+ verify(publisher, times(3)).publish(any());
+ verify(context).removeVariable(DomainConstants.LOOP_SEMAPHORE);
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/SubStateMachineHandlerTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/SubStateMachineHandlerTest.java
new file mode 100644
index 0000000000..be2a386017
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/handlers/SubStateMachineHandlerTest.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.pcext.handlers;
+
+import org.apache.seata.saga.engine.StateMachineConfig;
+import org.apache.seata.saga.engine.StateMachineEngine;
+import org.apache.seata.saga.engine.pcext.StateHandlerInterceptor;
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.engine.store.StateLogStore;
+import org.apache.seata.saga.proctrl.HierarchicalProcessContext;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.ExecutionStatus;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.impl.SubStateMachineImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link SubStateMachineHandler}
+ */
+public class SubStateMachineHandlerTest {
+
+ private SubStateMachineHandler handler;
+
+ @BeforeEach
+ public void setUp() {
+ handler = new SubStateMachineHandler();
+ }
+
+ @Test
+ public void addInterceptorAddToListTest() {
+ StateHandlerInterceptor interceptor =
mock(StateHandlerInterceptor.class);
+ handler.addInterceptor(interceptor);
+ assertTrue(handler.getInterceptors().contains(interceptor));
+ }
+
+ @Test
+ public void addInterceptorDuplicateInterceptorNotAddTest() {
+ StateHandlerInterceptor interceptor =
mock(StateHandlerInterceptor.class);
+ handler.addInterceptor(interceptor);
+ handler.addInterceptor(interceptor);
+ assertEquals(1, handler.getInterceptors().size());
+ }
+
+ @Test
+ public void addInterceptorMultipleDifferentInterceptorsTest() {
+ StateHandlerInterceptor interceptor1 =
mock(StateHandlerInterceptor.class);
+ StateHandlerInterceptor interceptor2 =
mock(StateHandlerInterceptor.class);
+ handler.addInterceptor(interceptor1);
+ handler.addInterceptor(interceptor2);
+ assertEquals(2, handler.getInterceptors().size());
+ assertTrue(handler.getInterceptors().contains(interceptor1));
+ assertTrue(handler.getInterceptors().contains(interceptor2));
+ }
+
+ @Test
+ public void getInterceptorsReturnListTest() {
+ List<StateHandlerInterceptor> interceptors = handler.getInterceptors();
+ assertNotNull(interceptors);
+ }
+
+ @Test
+ public void setInterceptorsTest() {
+ List<StateHandlerInterceptor> interceptors = new ArrayList<>();
+ StateHandlerInterceptor interceptor =
mock(StateHandlerInterceptor.class);
+ interceptors.add(interceptor);
+
+ handler.setInterceptors(interceptors);
+
+ assertSame(interceptors, handler.getInterceptors());
+ }
+
+ @Test
+ public void setInterceptorsWithNullTest() {
+ handler.setInterceptors(null);
+ assertNull(handler.getInterceptors());
+ }
+
+ @Test
+ public void addInterceptorWhenInterceptorsIsNullDoNothingTest() {
+ handler.setInterceptors(null);
+ StateHandlerInterceptor interceptor =
mock(StateHandlerInterceptor.class);
+
+ // Should not throw
+ assertDoesNotThrow(() -> handler.addInterceptor(interceptor));
+ }
+
+ @Test
+ public void handlerNotNullTest() {
+ assertNotNull(handler);
+ }
+
+ // ========== Existing Static Method Tests ==========
+
+ @Test
+ public void decideStatusForwardWithSuccessReturnSUTest() throws Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+ ExecutionStatus result = invokeDecideStatus(instance, true);
+
+ assertEquals(ExecutionStatus.SU, result);
+ }
+
+ @Test
+ public void decideStatusForwardWithFailureReturnInstanceStatusTest()
throws Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getStatus()).thenReturn(ExecutionStatus.FA);
+ when(instance.getCompensationStatus()).thenReturn(null);
+
+ ExecutionStatus result = invokeDecideStatus(instance, true);
+
+ assertEquals(ExecutionStatus.FA, result);
+ }
+
+ @Test
+ public void
decideStatusWhenCompensationStatusIsNullReturnInstanceStatusTest() throws
Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getStatus()).thenReturn(ExecutionStatus.FA);
+ when(instance.getCompensationStatus()).thenReturn(null);
+
+ ExecutionStatus result = invokeDecideStatus(instance, false);
+
+ assertEquals(ExecutionStatus.FA, result);
+ }
+
+ @Test
+ public void
decideStatusWhenCompensationStatusIsFAReturnInstanceStatusTest() throws
Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getStatus()).thenReturn(ExecutionStatus.FA);
+ when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.FA);
+
+ ExecutionStatus result = invokeDecideStatus(instance, false);
+
+ assertEquals(ExecutionStatus.FA, result);
+ }
+
+ @Test
+ public void decideStatusWhenCompensationStatusIsSUReturnFATest() throws
Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.SU);
+
+ ExecutionStatus result = invokeDecideStatus(instance, false);
+
+ assertEquals(ExecutionStatus.FA, result);
+ }
+
+ @Test
+ public void decideStatusWhenCompensationStatusIsUNReturnUNTest() throws
Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.UN);
+
+ ExecutionStatus result = invokeDecideStatus(instance, false);
+
+ assertEquals(ExecutionStatus.UN, result);
+ }
+
+ @Test
+ public void decideStatusWhenCompensationStatusIsRUReturnUNTest() throws
Exception {
+ StateMachineInstance instance = mock(StateMachineInstance.class);
+ when(instance.getCompensationStatus()).thenReturn(ExecutionStatus.RU);
+
+ ExecutionStatus result = invokeDecideStatus(instance, false);
+
+ assertEquals(ExecutionStatus.UN, result);
+ }
+
+ /**
+ * Invoke private static method decideStatus via reflection
+ */
+ private ExecutionStatus invokeDecideStatus(StateMachineInstance instance,
boolean isForward) throws Exception {
+ Method method = SubStateMachineHandler.class.getDeclaredMethod(
+ "decideStatus", StateMachineInstance.class, boolean.class);
+ method.setAccessible(true);
+ return (ExecutionStatus) method.invoke(null, instance, isForward);
+ }
+
+ // ========== New Process Tests ==========
+
+ @Test
+ public void processStartNewSubMachineTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ SubStateMachineImpl subStateMachine = mock(SubStateMachineImpl.class);
+ StateMachineEngine engine = mock(StateMachineEngine.class);
+ StateMachineInstance parentSmInstance =
mock(StateMachineInstance.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+ StateMachineInstance subSmInstance = mock(StateMachineInstance.class);
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(subStateMachine);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_ENGINE)).thenReturn(engine);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(parentSmInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+
+ // Input params
+ Map<String, Object> inputParams = new HashMap<>();
+ inputParams.put("a", 1);
+
when(context.getVariable(DomainConstants.VAR_NAME_INPUT_PARAMS)).thenReturn(inputParams);
+
+ // Mock SubStateMachine
+ when(subStateMachine.getStateMachineName()).thenReturn("subMachine");
+ when(subStateMachine.getName()).thenReturn("subState");
+
+ // Mock Engine Start
+ when(engine.start(eq("subMachine"), any(),
any())).thenReturn(subSmInstance);
+
+ // Mock SubStateMachineInstance Result
+ Map<String, Object> endParams = new HashMap<>();
+ endParams.put("result", "success");
+ when(subSmInstance.getEndParams()).thenReturn(endParams);
+ when(subSmInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+ when(subSmInstance.getCompensationStatus()).thenReturn(null);
+
+ // Mock StateInstance
+
when(stateInstance.getStateMachineInstance()).thenReturn(parentSmInstance);
+
+ handler.process(context);
+
+ verify(engine).start(eq("subMachine"), any(), any());
+ verify(stateInstance).setOutputParams(endParams);
+
verify(context).setVariable(eq(DomainConstants.VAR_NAME_OUTPUT_PARAMS),
eq(endParams));
+ verify(stateInstance).setStatus(ExecutionStatus.SU);
+ }
+
+ @Test
+ public void processForwardExistingSubMachineTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ SubStateMachineImpl subStateMachine = mock(SubStateMachineImpl.class);
+ StateMachineEngine engine = mock(StateMachineEngine.class);
+ StateMachineInstance parentSmInstance =
mock(StateMachineInstance.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+ StateMachineInstance subSmInstance = mock(StateMachineInstance.class);
+ StateMachineConfig config = mock(StateMachineConfig.class);
+ StateLogStore stateLogStore = mock(StateLogStore.class);
+
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(subStateMachine);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_ENGINE)).thenReturn(engine);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_INST)).thenReturn(parentSmInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONFIG)).thenReturn(config);
+
+ // Use Mockito to mock boolean check for forward flag
+
when(context.getVariable(DomainConstants.VAR_NAME_IS_FOR_SUB_STATMACHINE_FORWARD))
+ .thenReturn(true);
+
+ when(config.getStateLogStore()).thenReturn(stateLogStore);
+
+ // Correctly set up the retry scenario so validation passes
+
when(stateInstance.getStateIdRetriedFor()).thenReturn("originalStateId");
+ when(stateInstance.getMachineInstanceId()).thenReturn("machineId1");
+
+ StateInstance originalStateInst = mock(StateInstance.class);
+ when(originalStateInst.getId()).thenReturn("originalStateId");
+
when(originalStateInst.getMachineInstanceId()).thenReturn("machineId1");
+ // Must return null to break the do-while loop
+ when(originalStateInst.getStateIdRetriedFor()).thenReturn(null);
+
+ when(stateLogStore.getStateInstance(eq("originalStateId"),
any())).thenReturn(originalStateInst);
+
+ List<StateMachineInstance> existingSubInsts =
Collections.singletonList(subSmInstance);
+
when(stateLogStore.queryStateMachineInstanceByParentId(any())).thenReturn(existingSubInsts);
+
+ when(subSmInstance.getId()).thenReturn("subInstId");
+ when(subSmInstance.getEndParams()).thenReturn(new HashMap<>());
+ when(subSmInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+ // Mock Engine Forward
+ when(engine.forward(eq("subInstId"), any())).thenReturn(subSmInstance);
+
+ handler.process(context);
+
+ verify(engine).forward(eq("subInstId"), any());
+
verify(context).removeVariable(DomainConstants.VAR_NAME_IS_FOR_SUB_STATMACHINE_FORWARD);
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/interceptors/LoopTaskHandlerInterceptorTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/interceptors/LoopTaskHandlerInterceptorTest.java
new file mode 100644
index 0000000000..01a82be4b7
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/interceptors/LoopTaskHandlerInterceptorTest.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.pcext.interceptors;
+
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.engine.pcext.handlers.ServiceTaskStateHandler;
+import org.apache.seata.saga.engine.pcext.handlers.SubStateMachineHandler;
+import org.apache.seata.saga.engine.pcext.utils.LoopContextHolder;
+import org.apache.seata.saga.proctrl.HierarchicalProcessContext;
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.ExecutionStatus;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.TaskState.Loop;
+import org.apache.seata.saga.statelang.domain.impl.AbstractTaskState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link LoopTaskHandlerInterceptor}
+ */
+public class LoopTaskHandlerInterceptorTest {
+
+ private LoopTaskHandlerInterceptor interceptor;
+
+ @BeforeEach
+ public void setUp() {
+ interceptor = new LoopTaskHandlerInterceptor();
+ }
+
+ @Test
+ public void matchForServiceTaskHandlerReturnTrueTest() {
+ assertTrue(interceptor.match(ServiceTaskStateHandler.class));
+ }
+
+ @Test
+ public void matchForSubStateMachineHandlerReturnTrueTest() {
+ assertTrue(interceptor.match(SubStateMachineHandler.class));
+ }
+
+ @Test
+ public void matchForNullClassReturnFalseTest() {
+ assertFalse(interceptor.match(null));
+ }
+
+ @Test
+ public void preProcessWhenNotLoopStateDoNothingTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(false);
+
+ // Should not throw and do nothing when not a loop state
+ assertDoesNotThrow(() -> interceptor.preProcess(context));
+ }
+
+ @Test
+ public void postProcessWhenNotLoopStateDoNothingTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(false);
+
+ // Should not throw and do nothing when not a loop state
+ assertDoesNotThrow(() -> interceptor.postProcess(context, null));
+ }
+
+ @Test
+ public void interceptorNotNullTest() {
+ assertNotNull(interceptor);
+ }
+
+ // ========== Additional Tests: Coverage for preProcess Core Logic
==========
+
+ @Test
+ public void preProcessWhenIsLoopStateSetContextVariablesTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+ AbstractTaskState taskState = mock(AbstractTaskState.class);
+ Loop loop = mock(Loop.class);
+
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+
when(context.hasVariable(DomainConstants.VAR_NAME_CURRENT_COMPEN_TRIGGER_STATE))
+ .thenReturn(false);
+
when(context.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getState(context)).thenReturn(taskState);
+ when(taskState.getLoop()).thenReturn(loop);
+ when(context.getVariable(DomainConstants.LOOP_COUNTER)).thenReturn(0);
+
+ // Set loop configuration
+ when(loop.getElementIndexName()).thenReturn("loopIndex");
+ when(loop.getElementVariableName()).thenReturn("loopElement");
+
+ // Set LoopContextHolder
+ LoopContextHolder holder = new LoopContextHolder();
+ List<String> collection = Arrays.asList("item1", "item2", "item3");
+ holder.setCollection(collection);
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
+ // Set stateMachineContext
+ Map<String, Object> contextVariables = new HashMap<>();
+ contextVariables.put("existingKey", "existingValue");
+
when(context.getVariable(DomainConstants.VAR_NAME_STATEMACHINE_CONTEXT)).thenReturn(contextVariables);
+
+ // Execute
+ assertDoesNotThrow(() -> interceptor.preProcess(context));
+
+ // Verify setVariableLocally is called
+
verify(context).setVariableLocally(eq(DomainConstants.VAR_NAME_STATEMACHINE_CONTEXT),
any(Map.class));
+ }
+
+ // ========== Additional Tests: Coverage for postProcess Core Logic
==========
+
+ @Test
+ public void postProcessWhenStateSuccessIncrementCompletedInstancesTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+ when(stateInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+ LoopContextHolder holder = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+ .thenReturn(null);
+
+ // Execute
+ interceptor.postProcess(context, null);
+
+ // Verify nrOfCompletedInstances has been incremented
+ assertEquals(1, holder.getNrOfCompletedInstances().get());
+ // Verify nrOfActiveInstances has been decremented
+ assertEquals(-1, holder.getNrOfActiveInstances().get());
+ }
+
+ @Test
+ public void postProcessWhenStateFailedSetFailEndTrueTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+ when(stateInstance.getStatus()).thenReturn(ExecutionStatus.FA);
+
+ LoopContextHolder holder = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+ .thenReturn(null);
+
+ // Execute
+ interceptor.postProcess(context, null);
+
+ // Verify failEnd is set to true
+ assertTrue(holder.isFailEnd());
+ }
+
+ @Test
+ public void postProcessWhenExceptionOccursReleaseSemaphoreTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+ Semaphore semaphore = new Semaphore(0);
+
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+ when(stateInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+ LoopContextHolder holder = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+ .thenReturn(null);
+
when(context.hasVariable(DomainConstants.LOOP_SEMAPHORE)).thenReturn(true);
+
when(context.getVariable(DomainConstants.LOOP_SEMAPHORE)).thenReturn(semaphore);
+
+ Exception testException = new RuntimeException("test exception");
+
+ // Execute
+ interceptor.postProcess(context, testException);
+
+ // Verify semaphore is released
+ assertEquals(1, semaphore.availablePermits());
+ // Verify failEnd is set to true (due to exception)
+ assertTrue(holder.isFailEnd());
+ }
+
+ @Test
+ public void postProcessWhenStateInstanceIsNullDoNotSetFailEndTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(null);
+
+ LoopContextHolder holder = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+ .thenReturn(null);
+
+ // Execute
+ interceptor.postProcess(context, null);
+
+ // Verify failEnd remains false
+ assertFalse(holder.isFailEnd());
+ }
+
+ @Test
+ public void postProcessWhenLocalExceptionExistsSetFailEndTrueTest() {
+ HierarchicalProcessContext context =
mock(HierarchicalProcessContext.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+
+
when(context.hasVariable(DomainConstants.VAR_NAME_IS_LOOP_STATE)).thenReturn(true);
+
when(context.getVariable(DomainConstants.VAR_NAME_STATE_INST)).thenReturn(stateInstance);
+ when(stateInstance.getStatus()).thenReturn(ExecutionStatus.SU);
+
+ LoopContextHolder holder = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
+ // Local exception exists
+ Exception localException = new RuntimeException("local exception");
+
when(context.getVariableLocally(DomainConstants.VAR_NAME_CURRENT_EXCEPTION))
+ .thenReturn(localException);
+
+ // Execute
+ interceptor.postProcess(context, null);
+
+ // Verify failEnd is set to true
+ assertTrue(holder.isFailEnd());
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopContextHolderTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopContextHolderTest.java
new file mode 100644
index 0000000000..c1c29db2b5
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopContextHolderTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.pcext.utils;
+
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link LoopContextHolder}
+ */
+public class LoopContextHolderTest {
+
+ @Test
+ public void getCurrentWhenNotExistAndForceCreateFalseReturnNullTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(null);
+
+ LoopContextHolder result = LoopContextHolder.getCurrent(context,
false);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void getCurrentWhenNotExistAndForceCreateTrueCreateNewTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(null);
+
+ LoopContextHolder result = LoopContextHolder.getCurrent(context, true);
+
+ assertNotNull(result);
+ verify(context)
+
.setVariable(eq(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER),
any(LoopContextHolder.class));
+ }
+
+ @Test
+ public void getCurrentWhenExistReturnExistingTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ LoopContextHolder existing = new LoopContextHolder();
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(existing);
+
+ LoopContextHolder result = LoopContextHolder.getCurrent(context, true);
+
+ assertSame(existing, result);
+ }
+
+ @Test
+ public void clearCurrentRemoveVariableTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
+ LoopContextHolder.clearCurrent(context);
+
+
verify(context).removeVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER);
+ }
+
+ @Test
+ public void atomicCountersThreadSafeTest() {
+ LoopContextHolder holder = new LoopContextHolder();
+
+ holder.getNrOfInstances().set(10);
+ holder.getNrOfActiveInstances().incrementAndGet();
+ holder.getNrOfCompletedInstances().addAndGet(5);
+
+ assertEquals(10, holder.getNrOfInstances().get());
+ assertEquals(1, holder.getNrOfActiveInstances().get());
+ assertEquals(5, holder.getNrOfCompletedInstances().get());
+ }
+
+ @Test
+ public void stackOperationsTest() {
+ LoopContextHolder holder = new LoopContextHolder();
+
+ holder.getLoopCounterStack().push(1);
+ holder.getLoopCounterStack().push(2);
+ holder.getForwardCounterStack().push(10);
+
+ assertEquals(Integer.valueOf(2), holder.getLoopCounterStack().pop());
+ assertEquals(Integer.valueOf(1), holder.getLoopCounterStack().pop());
+ assertEquals(Integer.valueOf(10),
holder.getForwardCounterStack().pop());
+ }
+
+ @Test
+ public void collectionGetterSetterTest() {
+ LoopContextHolder holder = new LoopContextHolder();
+ List<String> collection = Arrays.asList("a", "b", "c");
+
+ holder.setCollection(collection);
+
+ assertSame(collection, holder.getCollection());
+ }
+
+ @Test
+ public void failEndFlagTest() {
+ LoopContextHolder holder = new LoopContextHolder();
+
+ assertFalse(holder.isFailEnd());
+ holder.setFailEnd(true);
+ assertTrue(holder.isFailEnd());
+ }
+
+ @Test
+ public void completionConditionSatisfiedFlagTest() {
+ LoopContextHolder holder = new LoopContextHolder();
+
+ assertFalse(holder.isCompletionConditionSatisfied());
+ holder.setCompletionConditionSatisfied(true);
+ assertTrue(holder.isCompletionConditionSatisfied());
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopTaskUtilsTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopTaskUtilsTest.java
new file mode 100644
index 0000000000..98fc71e570
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/pcext/utils/LoopTaskUtilsTest.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.pcext.utils;
+
+import org.apache.seata.saga.engine.pcext.StateInstruction;
+import org.apache.seata.saga.proctrl.ProcessContext;
+import org.apache.seata.saga.proctrl.impl.ProcessContextImpl;
+import org.apache.seata.saga.statelang.domain.DomainConstants;
+import org.apache.seata.saga.statelang.domain.State;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.apache.seata.saga.statelang.domain.StateType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Stack;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link LoopTaskUtils}
+ */
+public class LoopTaskUtilsTest {
+
+ @Test
+ public void matchLoopWhenStateTypeIsServiceTaskReturnTrueTest() {
+ State state = mock(State.class);
+ when(state.getType()).thenReturn(StateType.SERVICE_TASK);
+
+ assertTrue(LoopTaskUtils.matchLoop(state));
+ }
+
+ @Test
+ public void matchLoopWhenStateTypeIsScriptTaskReturnTrueTest() {
+ State state = mock(State.class);
+ when(state.getType()).thenReturn(StateType.SCRIPT_TASK);
+
+ assertTrue(LoopTaskUtils.matchLoop(state));
+ }
+
+ @Test
+ public void matchLoopWhenStateTypeIsSubStateMachineReturnTrueTest() {
+ State state = mock(State.class);
+ when(state.getType()).thenReturn(StateType.SUB_STATE_MACHINE);
+
+ assertTrue(LoopTaskUtils.matchLoop(state));
+ }
+
+ @Test
+ public void matchLoopWhenStateTypeIsChoiceReturnFalseTest() {
+ State state = mock(State.class);
+ when(state.getType()).thenReturn(StateType.CHOICE);
+
+ assertFalse(LoopTaskUtils.matchLoop(state));
+ }
+
+ @Test
+ public void matchLoopWhenStateIsNullReturnFalseTest() {
+ assertFalse(LoopTaskUtils.matchLoop(null));
+ }
+
+ @Test
+ public void reloadLoopCounterFromValidStateNameExtractCounterTest() {
+ String stateName = "myState" + LoopTaskUtils.LOOP_STATE_NAME_PATTERN +
"10";
+ int counter = LoopTaskUtils.reloadLoopCounter(stateName);
+
+ assertEquals(10, counter);
+ }
+
+ @Test
+ public void reloadLoopCounterFromInvalidStateNameReturnNegativeOneTest() {
+ String stateName = "myState";
+ int counter = LoopTaskUtils.reloadLoopCounter(stateName);
+
+ // Source code returns -1 when pattern is not found
+ assertEquals(-1, counter);
+ }
+
+ @Test
+ public void reloadLoopCounterWithZeroCounterTest() {
+ String stateName = "myState" + LoopTaskUtils.LOOP_STATE_NAME_PATTERN +
"0";
+ int counter = LoopTaskUtils.reloadLoopCounter(stateName);
+
+ assertEquals(0, counter);
+ }
+
+ @Test
+ public void reloadLoopCounterWithNullStateNameReturnNegativeOneTest() {
+ int counter = LoopTaskUtils.reloadLoopCounter(null);
+
+ assertEquals(-1, counter);
+ }
+
+ @Test
+ public void reloadLoopCounterWithEmptyStateNameReturnNegativeOneTest() {
+ int counter = LoopTaskUtils.reloadLoopCounter("");
+
+ assertEquals(-1, counter);
+ }
+
+ @Test
+ public void loopStateNamePatternConstantTest() {
+ assertEquals("-loop-", LoopTaskUtils.LOOP_STATE_NAME_PATTERN);
+ }
+
+ // ========== Additional Tests: Coverage for createLoopCounterContext
==========
+
+ @Test
+ public void createLoopCounterContextPushCountersToStackTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ LoopContextHolder holder = new LoopContextHolder();
+ List<String> collection = Arrays.asList("a", "b", "c");
+ holder.setCollection(collection);
+
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
+ LoopTaskUtils.createLoopCounterContext(context);
+
+ assertEquals(3, holder.getNrOfInstances().get());
+ // Stack should contain 2, 1, 0 (pushed in descending order)
+ Stack<Integer> stack = holder.getLoopCounterStack();
+ assertEquals(3, stack.size());
+ assertEquals(Integer.valueOf(0), stack.pop());
+ assertEquals(Integer.valueOf(1), stack.pop());
+ assertEquals(Integer.valueOf(2), stack.pop());
+ }
+
+ // ========== Additional Tests: Coverage for generateLoopStateName
==========
+
+ @Test
+ public void generateLoopStateNameWithValidNameAppendPatternTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ when(context.getVariable(DomainConstants.LOOP_COUNTER)).thenReturn(5);
+
+ String result = LoopTaskUtils.generateLoopStateName(context,
"testState");
+
+ assertEquals("testState-loop-5", result);
+ }
+
+ @Test
+ public void generateLoopStateNameWithBlankNameReturnOriginalTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
+ String result = LoopTaskUtils.generateLoopStateName(context, "");
+
+ assertEquals("", result);
+ }
+
+ @Test
+ public void generateLoopStateNameWithNullNameReturnNullTest() {
+ ProcessContext context = mock(ProcessContext.class);
+
+ String result = LoopTaskUtils.generateLoopStateName(context, null);
+
+ assertNull(result);
+ }
+
+ // ========== Additional Tests: Coverage for acquireNextLoopCounter
==========
+
+ @Test
+ public void acquireNextLoopCounterPopFromStackTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ LoopContextHolder holder = new LoopContextHolder();
+ holder.getLoopCounterStack().push(10);
+ holder.getLoopCounterStack().push(20);
+
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
+ int counter = LoopTaskUtils.acquireNextLoopCounter(context);
+
+ assertEquals(20, counter);
+ }
+
+ @Test
+ public void acquireNextLoopCounterWhenStackEmptyReturnNegativeOneTest() {
+ ProcessContext context = mock(ProcessContext.class);
+ LoopContextHolder holder = new LoopContextHolder();
+ // Empty stack
+
+
when(context.getVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER))
+ .thenReturn(holder);
+
+ int counter = LoopTaskUtils.acquireNextLoopCounter(context);
+
+ assertEquals(-1, counter);
+ }
+
+ // ========== Additional Tests: Coverage for createLoopEventContext
==========
+
+ @Test
+ public void
createLoopEventContextWithPositiveCounterSetCounterDirectlyTest() {
+ ProcessContext parentContext = mock(ProcessContext.class);
+ StateInstruction instruction = mock(StateInstruction.class);
+
when(parentContext.getInstruction(StateInstruction.class)).thenReturn(instruction);
+ when(instruction.getStateMachineName()).thenReturn("testMachine");
+ when(instruction.getTenantId()).thenReturn("tenant1");
+ when(instruction.getStateName()).thenReturn("testState");
+ when(instruction.getTemporaryState()).thenReturn(null);
+
+ ProcessContext childContext =
LoopTaskUtils.createLoopEventContext(parentContext, 5);
+
+ assertNotNull(childContext);
+ assertEquals(5,
childContext.getVariable(DomainConstants.LOOP_COUNTER));
+ }
+
+ @Test
+ public void
createLoopEventContextWithNegativeCounterAcquireFromStackTest() {
+ ProcessContextImpl parentContext = new ProcessContextImpl();
+ StateInstruction instruction = new StateInstruction();
+ instruction.setStateMachineName("testMachine");
+ instruction.setTenantId("tenant1");
+ instruction.setStateName("testState");
+ parentContext.setInstruction(instruction);
+
+ LoopContextHolder holder = new LoopContextHolder();
+ holder.getLoopCounterStack().push(7);
+
parentContext.setVariable(DomainConstants.VAR_NAME_CURRENT_LOOP_CONTEXT_HOLDER,
holder);
+
+ ProcessContext childContext =
LoopTaskUtils.createLoopEventContext(parentContext, -1);
+
+ assertNotNull(childContext);
+ assertEquals(7,
childContext.getVariable(DomainConstants.LOOP_COUNTER));
+ }
+
+ // ========== Additional Tests: Coverage for
findOutLastRetriedStateInstance ==========
+
+ @Test
+ public void findOutLastRetriedStateInstanceWhenFoundReturnStateTest() {
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateInstance state1 = mock(StateInstance.class);
+ StateInstance state2 = mock(StateInstance.class);
+
+ when(state1.getName()).thenReturn("state-loop-0");
+ when(state2.getName()).thenReturn("state-loop-1");
+
+ List<StateInstance> stateList = new ArrayList<>();
+ stateList.add(state1);
+ stateList.add(state2);
+
+ when(smInstance.getStateList()).thenReturn(stateList);
+
+ StateInstance result =
LoopTaskUtils.findOutLastRetriedStateInstance(smInstance, "state-loop-1");
+
+ assertSame(state2, result);
+ }
+
+ @Test
+ public void findOutLastRetriedStateInstanceWhenNotFoundReturnNullTest() {
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateInstance state1 = mock(StateInstance.class);
+
+ when(state1.getName()).thenReturn("state-loop-0");
+
+ List<StateInstance> stateList = new ArrayList<>();
+ stateList.add(state1);
+
+ when(smInstance.getStateList()).thenReturn(stateList);
+
+ StateInstance result =
LoopTaskUtils.findOutLastRetriedStateInstance(smInstance, "nonexistent");
+
+ assertNull(result);
+ }
+
+ @Test
+ public void findOutLastRetriedStateInstanceWithEmptyListReturnNullTest() {
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ when(smInstance.getStateList()).thenReturn(new ArrayList<>());
+
+ StateInstance result =
LoopTaskUtils.findOutLastRetriedStateInstance(smInstance, "anyState");
+
+ assertNull(result);
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/repo/impl/StateLogRepositoryImplTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/repo/impl/StateLogRepositoryImplTest.java
new file mode 100644
index 0000000000..6fa2edb3cd
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/repo/impl/StateLogRepositoryImplTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.repo.impl;
+
+import org.apache.seata.saga.engine.store.StateLogStore;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link StateLogRepositoryImpl}
+ */
+public class StateLogRepositoryImplTest {
+
+ @Test
+ public void getStateMachineInstanceWhenStoreIsNullReturnNullTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ assertNull(repo.getStateMachineInstance("any-id"));
+ }
+
+ @Test
+ public void getStateMachineInstanceWhenStoreExistsDelegateToStoreTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ StateLogStore store = mock(StateLogStore.class);
+ StateMachineInstance expected = mock(StateMachineInstance.class);
+ when(store.getStateMachineInstance("test-id")).thenReturn(expected);
+
+ repo.setStateLogStore(store);
+
+ assertSame(expected, repo.getStateMachineInstance("test-id"));
+ verify(store).getStateMachineInstance("test-id");
+ }
+
+ @Test
+ public void
getStateMachineInstanceByBusinessKeyWhenStoreIsNullReturnNullTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ assertNull(repo.getStateMachineInstanceByBusinessKey("key", "tenant"));
+ }
+
+ @Test
+ public void
getStateMachineInstanceByBusinessKeyWhenStoreExistsDelegateToStoreTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ StateLogStore store = mock(StateLogStore.class);
+ StateMachineInstance expected = mock(StateMachineInstance.class);
+ when(store.getStateMachineInstanceByBusinessKey("key",
"tenant")).thenReturn(expected);
+
+ repo.setStateLogStore(store);
+
+ assertSame(expected, repo.getStateMachineInstanceByBusinessKey("key",
"tenant"));
+ verify(store).getStateMachineInstanceByBusinessKey("key", "tenant");
+ }
+
+ @Test
+ public void
queryStateMachineInstanceByParentIdWhenStoreIsNullReturnNullTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ assertNull(repo.queryStateMachineInstanceByParentId("parent-id"));
+ }
+
+ @Test
+ public void
queryStateMachineInstanceByParentIdWhenStoreExistsDelegateToStoreTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ StateLogStore store = mock(StateLogStore.class);
+ List<StateMachineInstance> expected =
Arrays.asList(mock(StateMachineInstance.class));
+
when(store.queryStateMachineInstanceByParentId("parent-id")).thenReturn(expected);
+
+ repo.setStateLogStore(store);
+
+ assertSame(expected,
repo.queryStateMachineInstanceByParentId("parent-id"));
+ verify(store).queryStateMachineInstanceByParentId("parent-id");
+ }
+
+ @Test
+ public void getStateInstanceWhenStoreIsNullReturnNullTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ assertNull(repo.getStateInstance("state-id", "machine-id"));
+ }
+
+ @Test
+ public void getStateInstanceWhenStoreExistsDelegateToStoreTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ StateLogStore store = mock(StateLogStore.class);
+ StateInstance expected = mock(StateInstance.class);
+ when(store.getStateInstance("state-id",
"machine-id")).thenReturn(expected);
+
+ repo.setStateLogStore(store);
+
+ assertSame(expected, repo.getStateInstance("state-id", "machine-id"));
+ verify(store).getStateInstance("state-id", "machine-id");
+ }
+
+ @Test
+ public void
queryStateInstanceListByMachineInstanceIdWhenStoreIsNullReturnNullTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+
assertNull(repo.queryStateInstanceListByMachineInstanceId("machine-id"));
+ }
+
+ @Test
+ public void
queryStateInstanceListByMachineInstanceIdWhenStoreExistsDelegateToStoreTest() {
+ StateLogRepositoryImpl repo = new StateLogRepositoryImpl();
+ StateLogStore store = mock(StateLogStore.class);
+ List<StateInstance> expected =
Arrays.asList(mock(StateInstance.class));
+
when(store.queryStateInstanceListByMachineInstanceId("machine-id")).thenReturn(expected);
+
+ repo.setStateLogStore(store);
+
+ assertSame(expected,
repo.queryStateInstanceListByMachineInstanceId("machine-id"));
+ verify(store).queryStateInstanceListByMachineInstanceId("machine-id");
+ }
+}
diff --git
a/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/utils/ExceptionUtilsTest.java
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/utils/ExceptionUtilsTest.java
new file mode 100644
index 0000000000..0c5b37d1b9
--- /dev/null
+++
b/saga/seata-saga-engine/src/test/java/org/apache/seata/saga/engine/utils/ExceptionUtilsTest.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.seata.saga.engine.utils;
+
+import org.apache.seata.common.exception.FrameworkErrorCode;
+import org.apache.seata.saga.engine.exception.EngineExecutionException;
+import org.apache.seata.saga.statelang.domain.StateInstance;
+import org.apache.seata.saga.statelang.domain.StateMachine;
+import org.apache.seata.saga.statelang.domain.StateMachineInstance;
+import org.junit.jupiter.api.Test;
+
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for {@link ExceptionUtils}
+ */
+public class ExceptionUtilsTest {
+
+ @Test
+ public void createEngineExecutionExceptionWithAllParamsTest() {
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachine sm = mock(StateMachine.class);
+ StateInstance stateInstance = mock(StateInstance.class);
+
+ when(smInstance.getStateMachine()).thenReturn(sm);
+ when(sm.getAppName()).thenReturn("testApp");
+ when(smInstance.getId()).thenReturn("sm-123");
+ when(stateInstance.getName()).thenReturn("testState");
+ when(stateInstance.getId()).thenReturn("state-456");
+
+ Exception cause = new RuntimeException("test");
+ EngineExecutionException result =
ExceptionUtils.createEngineExecutionException(
+ cause, FrameworkErrorCode.UnknownAppError, "test message",
smInstance, stateInstance);
+
+ assertEquals("test message", result.getMessage());
+ assertEquals("testApp", result.getStateMachineName());
+ assertEquals("sm-123", result.getStateMachineInstanceId());
+ assertEquals("testState", result.getStateName());
+ assertEquals("state-456", result.getStateInstanceId());
+ }
+
+ @Test
+ public void
createEngineExecutionExceptionWithNullStateMachineInstanceTest() {
+ EngineExecutionException result =
ExceptionUtils.createEngineExecutionException(
+ null, FrameworkErrorCode.UnknownAppError, "test", null,
(StateInstance) null);
+
+ assertNull(result.getStateMachineName());
+ assertNull(result.getStateMachineInstanceId());
+ }
+
+ @Test
+ public void createEngineExecutionExceptionWithStateNameTest() {
+ StateMachineInstance smInstance = mock(StateMachineInstance.class);
+ StateMachine sm = mock(StateMachine.class);
+
+ when(smInstance.getStateMachine()).thenReturn(sm);
+ when(sm.getAppName()).thenReturn("testApp");
+ when(smInstance.getId()).thenReturn("sm-123");
+
+ Exception cause = new RuntimeException("test");
+ EngineExecutionException result =
ExceptionUtils.createEngineExecutionException(
+ cause, FrameworkErrorCode.UnknownAppError, "test message",
smInstance, "testStateName");
+
+ assertEquals("test message", result.getMessage());
+ assertEquals("testApp", result.getStateMachineName());
+ assertEquals("sm-123", result.getStateMachineInstanceId());
+ assertEquals("testStateName", result.getStateName());
+ }
+
+ @Test
+ public void getNetExceptionTypeSocketTimeoutExceptionConnectTimeoutTest() {
+ SocketTimeoutException e = new SocketTimeoutException("connect timed
out");
+
assertEquals(ExceptionUtils.NetExceptionType.CONNECT_TIMEOUT_EXCEPTION,
ExceptionUtils.getNetExceptionType(e));
+ }
+
+ @Test
+ public void getNetExceptionTypeSocketTimeoutExceptionReadTimeoutTest() {
+ SocketTimeoutException e = new SocketTimeoutException("read timed
out");
+ assertEquals(ExceptionUtils.NetExceptionType.READ_TIMEOUT_EXCEPTION,
ExceptionUtils.getNetExceptionType(e));
+ }
+
+ @Test
+ public void getNetExceptionTypeConnectExceptionTest() {
+ ConnectException e = new ConnectException("Connection refused");
+ assertEquals(ExceptionUtils.NetExceptionType.CONNECT_EXCEPTION,
ExceptionUtils.getNetExceptionType(e));
+ }
+
+ @Test
+ public void getNetExceptionTypeNestedNetworkExceptionTest() {
+ ConnectException innerException = new ConnectException();
+ RuntimeException outerException = new RuntimeException("wrapper",
innerException);
+ assertEquals(
+ ExceptionUtils.NetExceptionType.CONNECT_EXCEPTION,
ExceptionUtils.getNetExceptionType(outerException));
+ }
+
+ @Test
+ public void getNetExceptionTypeNotNetExceptionTest() {
+ RuntimeException e = new RuntimeException("not a network error");
+ assertEquals(ExceptionUtils.NetExceptionType.NOT_NET_EXCEPTION,
ExceptionUtils.getNetExceptionType(e));
+ }
+
+ @Test
+ public void getNetExceptionTypeMaxCauseDepthExceededTest() {
+ Exception current = new RuntimeException("base");
+ for (int i = 0; i < 25; i++) {
+ current = new RuntimeException("level-" + i, current);
+ }
+ assertEquals(ExceptionUtils.NetExceptionType.NOT_NET_EXCEPTION,
ExceptionUtils.getNetExceptionType(current));
+ }
+
+ @Test
+ public void isNetExceptionWhenIsNetworkExceptionReturnTrueTest() {
+ ConnectException e = new ConnectException();
+ assertTrue(ExceptionUtils.isNetException(e));
+ }
+
+ @Test
+ public void isNetExceptionWhenNotNetworkExceptionReturnFalseTest() {
+ RuntimeException e = new RuntimeException("not network");
+ assertFalse(ExceptionUtils.isNetException(e));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]