From c3c63c2cdedb68a55b6ace6e57c399d8293ba6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Thu, 18 Dec 2025 18:14:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0bid/tid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../machine/command/TransactionExecutor.java | 8 +- .../machine/instruction/CallbackConfig.java | 23 + .../instruction/InstructionContext.java | 19 + .../machine/mqtt/MqttCallbackRegistry.java | 110 +++- .../machine/mqtt/store/MqttCallbackInfo.java | 20 + .../ComprehensiveDrcStateMachineTest.java | 605 ++++++++++++++++++ .../machine/vendor/test/TestVendorConfig.java | 158 +++++ .../TestCheckConditionInstruction.java | 59 ++ .../instruction/TestCleanupInstruction.java | 53 ++ .../TestComplexCleanupInstruction.java | 52 ++ .../TestComplexFailureInstruction.java | 52 ++ .../TestComplexRootInstruction.java | 57 ++ .../TestComplexSuccessInstruction.java | 52 ++ .../TestFailureSubInstruction.java | 53 ++ .../test/instruction/TestMainInstruction.java | 58 ++ .../TestMethodTimeoutInstruction.java | 54 ++ .../instruction/TestRejectedInstruction.java | 55 ++ .../TestRemoteFailInstruction.java | 43 ++ .../instruction/TestRetryInstruction.java | 53 ++ .../TestSimpleSuccessInstruction.java | 60 ++ .../TestStateTimeoutInstruction.java | 62 ++ .../TestSuccessSubInstruction.java | 53 ++ .../TestTidBidMatchInstruction.java | 78 +++ 23 files changed, 1833 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/tuoheng/machine/ComprehensiveDrcStateMachineTest.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/TestVendorConfig.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCheckConditionInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCleanupInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexCleanupInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexFailureInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexRootInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexSuccessInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestFailureSubInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMainInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMethodTimeoutInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRejectedInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRemoteFailInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRetryInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSimpleSuccessInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestStateTimeoutInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSuccessSubInstruction.java create mode 100644 src/test/java/com/tuoheng/machine/vendor/test/instruction/TestTidBidMatchInstruction.java diff --git a/src/main/java/com/tuoheng/machine/command/TransactionExecutor.java b/src/main/java/com/tuoheng/machine/command/TransactionExecutor.java index 79f5271..504abdd 100644 --- a/src/main/java/com/tuoheng/machine/command/TransactionExecutor.java +++ b/src/main/java/com/tuoheng/machine/command/TransactionExecutor.java @@ -238,7 +238,7 @@ public class TransactionExecutor { CompletableFuture future = new CompletableFuture<>(); AtomicBoolean callbackReceived = new AtomicBoolean(false); - // 注册回调 + // 注册回调(包含 tid/bid 过滤) String callbackId = callbackRegistry.registerCallback( callbackConfig.getTopic(), messageBody -> { @@ -251,7 +251,11 @@ public class TransactionExecutor { } } }, - callbackConfig.getTimeoutMs() + callbackConfig.getTimeoutMs(), + callbackConfig.getTidFieldPath(), + callbackConfig.getExpectedTid(), + callbackConfig.getBidFieldPath(), + callbackConfig.getExpectedBid() ); // 设置超时(不阻塞线程) diff --git a/src/main/java/com/tuoheng/machine/instruction/CallbackConfig.java b/src/main/java/com/tuoheng/machine/instruction/CallbackConfig.java index edb8c9d..41e8ea0 100644 --- a/src/main/java/com/tuoheng/machine/instruction/CallbackConfig.java +++ b/src/main/java/com/tuoheng/machine/instruction/CallbackConfig.java @@ -39,16 +39,39 @@ public class CallbackConfig { /** * 超时时间(毫秒) */ + @Builder.Default private long timeoutMs = 3000; + /** + * 事务ID字段路径(用于匹配回调消息,如 "tid") + */ + private String tidFieldPath; + + /** + * 业务ID字段路径(用于匹配回调消息,如 "bid") + */ + private String bidFieldPath; + + /** + * 期望的事务ID值(从InstructionContext中获取) + */ + private String expectedTid; + + /** + * 期望的业务ID值(从InstructionContext中获取) + */ + private String expectedBid; + /** * 判断消息是否匹配 + * 注意:tid/bid 的匹配已经在 MqttCallbackRegistry 注册层完成,这里只检查业务字段 */ public boolean matches(Object messageBody) { if (customPredicate != null) { return customPredicate.test(messageBody); } + // 检查业务字段是否匹配 Object fieldValue = extractFieldValue(messageBody, fieldPath); return expectedValue == null || expectedValue.equals(fieldValue); } diff --git a/src/main/java/com/tuoheng/machine/instruction/InstructionContext.java b/src/main/java/com/tuoheng/machine/instruction/InstructionContext.java index 46fd54f..33a4c78 100644 --- a/src/main/java/com/tuoheng/machine/instruction/InstructionContext.java +++ b/src/main/java/com/tuoheng/machine/instruction/InstructionContext.java @@ -5,6 +5,7 @@ import lombok.Data; import java.util.HashMap; import java.util.Map; +import java.util.UUID; /** * 指令执行上下文 @@ -36,15 +37,33 @@ public class InstructionContext { */ private Map commandParams = new HashMap<>(); + /** + * 事务ID(Transaction ID)- 用于匹配回调消息 + * 在命令执行阶段生成,用于标识本次指令执行 + */ + private String tid; + + /** + * 业务ID(Business ID)- 用于匹配回调消息 + * 在命令执行阶段生成,用于标识本次业务操作 + */ + private String bid; + public InstructionContext(String sn, String vendorType) { this.sn = sn; this.vendorType = vendorType; + // 自动生成 tid 和 bid + this.tid = UUID.randomUUID().toString(); + this.bid = UUID.randomUUID().toString(); } public InstructionContext(String sn, String vendorType, MqttClient mqttClient) { this.sn = sn; this.vendorType = vendorType; this.mqttClient = mqttClient; + // 自动生成 tid 和 bid + this.tid = UUID.randomUUID().toString(); + this.bid = UUID.randomUUID().toString(); } public void putContextData(String key, Object value) { diff --git a/src/main/java/com/tuoheng/machine/mqtt/MqttCallbackRegistry.java b/src/main/java/com/tuoheng/machine/mqtt/MqttCallbackRegistry.java index 0cc3b2d..250c536 100644 --- a/src/main/java/com/tuoheng/machine/mqtt/MqttCallbackRegistry.java +++ b/src/main/java/com/tuoheng/machine/mqtt/MqttCallbackRegistry.java @@ -86,6 +86,24 @@ public class MqttCallbackRegistry { * @return 回调ID(用于取消注册) */ public String registerCallback(String topic, Consumer messageHandler, long timeoutMs) { + return registerCallback(topic, messageHandler, timeoutMs, null, null, null, null); + } + + /** + * 注册回调(支持 tid/bid 过滤) + * + * @param topic 监听的主题 + * @param messageHandler 消息处理器 + * @param timeoutMs 超时时间(毫秒) + * @param tidFieldPath tid 字段路径(如 "tid") + * @param expectedTid 期望的 tid 值 + * @param bidFieldPath bid 字段路径(如 "bid") + * @param expectedBid 期望的 bid 值 + * @return 回调ID(用于取消注册) + */ + public String registerCallback(String topic, Consumer messageHandler, long timeoutMs, + String tidFieldPath, String expectedTid, + String bidFieldPath, String expectedBid) { String callbackId = UUID.randomUUID().toString(); // 1. 创建回调信息并存储到存储层 @@ -95,6 +113,10 @@ public class MqttCallbackRegistry { .timeoutMs(timeoutMs) .registerTime(System.currentTimeMillis()) .nodeId(nodeId) + .tidFieldPath(tidFieldPath) + .expectedTid(expectedTid) + .bidFieldPath(bidFieldPath) + .expectedBid(expectedBid) .build(); callbackStore.registerCallback(callbackInfo); @@ -102,8 +124,8 @@ public class MqttCallbackRegistry { // 2. 将 Consumer 存储到本地内存 localHandlers.put(callbackId, messageHandler); - log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}", - callbackId, topic, timeoutMs, nodeId); + log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}, tid={}, bid={}", + callbackId, topic, timeoutMs, nodeId, expectedTid, expectedBid); return callbackId; } @@ -157,6 +179,13 @@ public class MqttCallbackRegistry { continue; } + // 检查 tid/bid 是否匹配(如果配置了) + if (!matchesTidBid(callbackInfo, messageBody)) { + log.debug("MQTT消息 tid/bid 不匹配,跳过回调: callbackId={}, topic={}", + callbackInfo.getCallbackId(), topic); + continue; + } + // 判断回调是在本节点还是其他节点 if (nodeId.equals(callbackInfo.getNodeId())) { // 本节点的回调,直接执行 @@ -178,6 +207,83 @@ public class MqttCallbackRegistry { } } + /** + * 检查消息的 tid/bid 是否匹配 + * + * @param callbackInfo 回调信息 + * @param messageBody 消息体 + * @return true 如果匹配或未配置 tid/bid,false 如果不匹配 + */ + private boolean matchesTidBid(MqttCallbackInfo callbackInfo, Object messageBody) { + // 1. 检查 tid 是否匹配(如果配置了) + if (callbackInfo.getTidFieldPath() != null && callbackInfo.getExpectedTid() != null) { + Object tidValue = extractFieldValue(messageBody, callbackInfo.getTidFieldPath()); + if (!callbackInfo.getExpectedTid().equals(tidValue)) { + log.debug("tid 不匹配: expected={}, actual={}", callbackInfo.getExpectedTid(), tidValue); + return false; // tid 不匹配 + } + } + + // 2. 检查 bid 是否匹配(如果配置了) + if (callbackInfo.getBidFieldPath() != null && callbackInfo.getExpectedBid() != null) { + Object bidValue = extractFieldValue(messageBody, callbackInfo.getBidFieldPath()); + if (!callbackInfo.getExpectedBid().equals(bidValue)) { + log.debug("bid 不匹配: expected={}, actual={}", callbackInfo.getExpectedBid(), bidValue); + return false; // bid 不匹配 + } + } + + // 3. tid/bid 都匹配或未配置 + return true; + } + + /** + * 从消息体中提取字段值 + * + * @param messageBody 消息体 + * @param fieldPath 字段路径(支持嵌套,如 "data.status") + * @return 字段值 + */ + private Object extractFieldValue(Object messageBody, String fieldPath) { + if (messageBody == null || fieldPath == null) { + return null; + } + + // 如果 messageBody 是字符串,尝试解析为 JSON + Object current = messageBody; + if (messageBody instanceof String) { + try { + current = objectMapper.readValue((String) messageBody, Object.class); + } catch (Exception e) { + log.warn("解析消息体失败: {}", messageBody); + return null; + } + } + + String[] parts = fieldPath.split("\\."); + + for (String part : parts) { + if (current == null) { + return null; + } + + if (current instanceof Map) { + current = ((Map) current).get(part); + } else { + try { + java.lang.reflect.Field field = current.getClass().getDeclaredField(part); + field.setAccessible(true); + current = field.get(current); + } catch (Exception e) { + log.debug("提取字段失败: fieldPath={}, part={}", fieldPath, part); + return null; + } + } + } + + return current; + } + /** * 执行本地回调 * diff --git a/src/main/java/com/tuoheng/machine/mqtt/store/MqttCallbackInfo.java b/src/main/java/com/tuoheng/machine/mqtt/store/MqttCallbackInfo.java index e358e05..ed58797 100644 --- a/src/main/java/com/tuoheng/machine/mqtt/store/MqttCallbackInfo.java +++ b/src/main/java/com/tuoheng/machine/mqtt/store/MqttCallbackInfo.java @@ -43,6 +43,26 @@ public class MqttCallbackInfo implements Serializable { */ private String nodeId; + /** + * 事务ID字段路径(用于匹配回调消息,如 "tid") + */ + private String tidFieldPath; + + /** + * 期望的事务ID值 + */ + private String expectedTid; + + /** + * 业务ID字段路径(用于匹配回调消息,如 "bid") + */ + private String bidFieldPath; + + /** + * 期望的业务ID值 + */ + private String expectedBid; + /** * 是否已超时 */ diff --git a/src/test/java/com/tuoheng/machine/ComprehensiveDrcStateMachineTest.java b/src/test/java/com/tuoheng/machine/ComprehensiveDrcStateMachineTest.java new file mode 100644 index 0000000..8fa9384 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/ComprehensiveDrcStateMachineTest.java @@ -0,0 +1,605 @@ +package com.tuoheng.machine; + +import com.tuoheng.machine.command.CommandResult; +import com.tuoheng.machine.command.CommandType; +import com.tuoheng.machine.mqtt.MqttCallbackRegistry; +import com.tuoheng.machine.state.*; +import com.tuoheng.machine.vendor.VendorRegistry; +import com.tuoheng.machine.vendor.test.TestVendorConfig; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * DRC状态机综合测试 + * 测试各种场景: + * 1. 指令被通过/拒绝 + * 2. 指令执行远程命令成功/失败 + * 3. 指令回复超时/不超时 + * 4. 状态回复超时/不超时 + * 5. 指令包含成功子命令/失败子命令/always子命令 + */ +@SpringBootTest +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class ComprehensiveDrcStateMachineTest { + + @Autowired + MachineCommandManager machineCommandManager; + + @Autowired + MqttCallbackRegistry mqttCallbackRegistry; + + @Autowired + VendorRegistry vendorRegistry; + + private static final ScheduledExecutorService scheduler = + Executors.newScheduledThreadPool(4); + + private static final String TEST_SN = "TEST_SN_001"; + + @BeforeAll + public void setup() { + log.info("=== 开始综合测试初始化 ==="); + + // 注册测试厂家配置 + TestVendorConfig testVendorConfig = new TestVendorConfig(); + vendorRegistry.registerVendor(testVendorConfig); + + // 绑定SN到测试厂家 + vendorRegistry.bindSnToVendor(TEST_SN, "TEST"); + + // 初始化机器状态 + MachineStates initialStates = new MachineStates(); + initialStates.setAirportState(AirportState.ONLINE); + initialStates.setDroneState(DroneState.ONLINE); + machineCommandManager.updateMachineStates(TEST_SN, initialStates, true); + + log.info("=== 综合测试初始化完成 ==="); + } + + @BeforeEach + public void beforeEach(TestInfo testInfo) { + log.info("\n\n========================================"); + log.info("开始测试: {}", testInfo.getDisplayName()); + log.info("========================================\n"); + } + + @AfterEach + public void afterEach(TestInfo testInfo) { + log.info("\n========================================"); + log.info("完成测试: {}", testInfo.getDisplayName()); + log.info("========================================\n\n"); + } + + /** + * 测试1: 简单成功场景 + * 指令被通过,远程命令成功,方法回调和状态回调都成功 + */ + @Test + @Order(1) + @DisplayName("测试1: 简单成功场景 - 指令通过,远程命令成功,回调都成功") + public void testSimpleSuccess() throws ExecutionException, InterruptedException { + log.info(">>> 场景:指令被通过,远程命令成功,方法回调和状态回调都成功"); + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.TAKE_OFF, new HashMap<>()); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 发送方法回调 + Thread.sleep(100); + String response = "{\"result\":\"success\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送方法回调: {}", response); + + // 2. 发送状态回调 + Thread.sleep(100); + response = "{\"status\":\"completed\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/state", response); + log.info(">>> 模拟发送状态回调: {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "指令应该执行成功"); + log.info(">>> 测试通过:指令执行成功"); + } + + /** + * 测试2: 远程命令失败场景 + * 指令被通过,但远程命令执行失败(抛出异常) + */ + @Test + @Order(2) + @DisplayName("测试2: 远程命令失败场景 - 指令通过,但远程命令执行失败") + public void testRemoteCommandFail() throws ExecutionException, InterruptedException { + log.info(">>> 场景:指令被通过,但远程命令执行时抛出异常"); + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.EMERGENCY_STOP, new HashMap<>()); + + CommandResult result = future.get(); + assertFalse(result.isSuccess(), "指令应该执行失败"); + assertNotNull(result.getErrorMessage(), "应该有错误消息"); + log.info(">>> 测试通过:远程命令失败,错误消息: {}", result.getErrorMessage()); + } + + /** + * 测试3: 方法回调超时场景 + * 远程命令发送成功,但方法回调超时(不发送回调消息) + */ + @Test + @Order(3) + @DisplayName("测试3: 方法回调超时场景 - 远程命令成功,但方法回调超时") + public void testMethodCallbackTimeout() throws ExecutionException, InterruptedException { + log.info(">>> 场景:远程命令发送成功,但方法回调超时"); + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.RESUME_FLIGHT, new HashMap<>()); + + // 不发送任何回调消息,让它超时 + log.info(">>> 不发送方法回调消息,等待超时..."); + + CommandResult result = future.get(); + assertFalse(result.isSuccess(), "指令应该因超时而失败"); + assertTrue(result.getErrorMessage().contains("超时") || result.getErrorMessage().contains("timeout"), + "错误消息应该包含超时信息"); + log.info(">>> 测试通过:方法回调超时,错误消息: {}", result.getErrorMessage()); + } + + /** + * 测试4: 状态回调超时场景 + * 方法回调成功,但状态回调超时 + */ + @Test + @Order(4) + @DisplayName("测试4: 状态回调超时场景 - 方法回调成功,但状态回调超时") + public void testStateCallbackTimeout() throws ExecutionException, InterruptedException { + log.info(">>> 场景:方法回调成功,但状态回调超时"); + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.POINT_FLY, new HashMap<>()); + + // 只发送方法回调,不发送状态回调 + scheduler.schedule(() -> { + try { + Thread.sleep(100); + String response = "{\"result\":\"success\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送方法回调: {}", response); + log.info(">>> 不发送状态回调消息,等待超时..."); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertFalse(result.isSuccess(), "指令应该因状态回调超时而失败"); + assertTrue(result.getErrorMessage().contains("超时") || result.getErrorMessage().contains("timeout"), + "错误消息应该包含超时信息"); + log.info(">>> 测试通过:状态回调超时,错误消息: {}", result.getErrorMessage()); + } + + /** + * 测试5: 成功子命令场景 + * 主指令成功后执行成功分支的子指令 + */ + @Test + @Order(5) + @DisplayName("测试5: 成功子命令场景 - 主指令成功后执行成功分支子指令") + public void testSuccessSubCommand() throws ExecutionException, InterruptedException { + log.info(">>> 场景:主指令成功后执行成功分支的子指令"); + + Map params = new HashMap<>(); + params.put("shouldFail", false); // 主指令成功 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.OPEN_COVER, params); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 主指令的方法回调(成功) + Thread.sleep(100); + String response = "{\"result\":\"success\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送主指令方法回调(成功): {}", response); + + // 2. 成功子指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"subSuccess\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送成功子指令方法回调: ", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "指令应该执行成功(包括子指令)"); + log.info(">>> 测试通过:主指令和成功子指令都执行成功"); + } + + /** + * 测试6: 失败子命令场景 + * 主指令失败后执行失败分支的补救子指令 + */ + @Test + @Order(6) + @DisplayName("测试6: 失败子命令场景 - 主指令失败后执行失败分支补救子指令") + public void testFailureSubCommand() throws ExecutionException, InterruptedException { + log.info(">>> 场景:主指令失败后执行失败分支的补救子指令"); + + Map params = new HashMap<>(); + params.put("shouldFail", true); // 主指令失败 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.CLOSE_COVER, params); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 主指令的方法回调(失败) + Thread.sleep(100); + String response = "{\"result\":\"fail\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送主指令方法回调(失败): {}", response); + + // 2. 失败补救子指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"remedySuccess\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送失败补救子指令方法回调: {}", response); + + // 3. 重试子指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"retrySuccess\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送重试子指令方法回调: {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "指令应该通过补救子指令执行成功"); + log.info(">>> 测试通过:主指令失败,但通过补救子指令和重试成功"); + } + + /** + * 测试7: Always子命令场景(主指令成功) + * 主指令成功,无论如何都执行清理指令 + */ + @Test + @Order(7) + @DisplayName("测试7: Always子命令场景(主指令成功)- 无论成功失败都执行清理") + public void testAlwaysSubCommandWithSuccess() throws ExecutionException, InterruptedException { + log.info(">>> 场景:主指令成功,无论如何都执行清理指令"); + + Map params = new HashMap<>(); + params.put("mainShouldFail", false); // 主指令成功 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.START_MISSION, params); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 主指令的方法回调(成功) + Thread.sleep(100); + String response = "{\"result\":\"success\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送主指令方法回调(成功): {}", response); + + // 2. 清理指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"cleanupSuccess\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送清理指令方法回调: {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "指令应该执行成功(包括清理指令)"); + log.info(">>> 测试通过:主指令成功,清理指令也执行成功"); + } + + /** + * 测试8: Always子命令场景(主指令失败) + * 主指令失败,但仍然执行清理指令 + */ + @Test + @Order(8) + @DisplayName("测试8: Always子命令场景(主指令失败)- 主指令失败仍执行清理") + public void testAlwaysSubCommandWithFailure() throws ExecutionException, InterruptedException { + log.info(">>> 场景:主指令失败,但仍然执行清理指令"); + + Map params = new HashMap<>(); + params.put("mainShouldFail", true); // 主指令失败 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.START_MISSION, params); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 主指令的方法回调(失败) + Thread.sleep(100); + String response = "{\"result\":\"fail\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送主指令方法回调(失败): {}", response); + + // 2. 清理指令的方法回调(仍然执行) + Thread.sleep(100); + response = "{\"result\":\"cleanupSuccess\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送清理指令方法回调: {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "清理指令应该执行成功(即使主指令失败)"); + log.info(">>> 测试通过:主指令失败,但清理指令执行成功"); + } + + /** + * 测试9: 复杂指令树场景(成功路径) + * 测试复杂的多层嵌套指令树,走成功分支 + */ + @Test + @Order(9) + @DisplayName("测试9: 复杂指令树场景(成功路径)- 多层嵌套,走成功分支") + public void testComplexInstructionTreeSuccess() throws ExecutionException, InterruptedException { + log.info(">>> 场景:复杂的多层嵌套指令树,走成功分支"); + + Map params = new HashMap<>(); + params.put("complexRootShouldFail", false); // 根指令成功 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.ENTER_DRC_MODE, params); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 根指令的方法回调(成功) + Thread.sleep(100); + String response = "{\"result\":\"success\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送根指令方法回调(成功): {}", response); + + // 2. 成功分支指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"complexSuccess\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送成功分支指令方法回调: {}", response); + + // 3. 清理指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"complexCleanup\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送清理指令方法回调: {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "复杂指令树应该执行成功"); + log.info(">>> 测试通过:复杂指令树成功路径执行成功"); + } + + /** + * 测试10: 复杂指令树场景(失败路径) + * 测试复杂的多层嵌套指令树,走失败分支 + */ + @Test + @Order(10) + @DisplayName("测试10: 复杂指令树场景(失败路径)- 多层嵌套,走失败分支") + public void testComplexInstructionTreeFailure() throws ExecutionException, InterruptedException { + log.info(">>> 场景:复杂的多层嵌套指令树,走失败分支"); + + Map params = new HashMap<>(); + params.put("complexRootShouldFail", true); // 根指令失败 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.ENTER_DRC_MODE, params); + + // 模拟设备响应 + scheduler.schedule(() -> { + try { + // 1. 根指令的方法回调(失败) + Thread.sleep(100); + String response = "{\"result\":\"fail\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送根指令方法回调(失败): {}", response); + + // 2. 失败分支指令的方法回调 + Thread.sleep(100); + response = "{\"result\":\"complexFailure\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送失败分支指令方法回调: {}", response); + + // 3. 清理指令的方法回调(仍然执行) + Thread.sleep(100); + response = "{\"result\":\"complexCleanup\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送清理指令方法回调: ", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "复杂指令树应该通过失败分支和清理成功"); + log.info(">>> 测试通过:复杂指令树失败路径执行成功"); + } + + /** + * 测试11: 指令被拒绝场景 + * canExecute返回false,指令不会被执行 + */ + @Test + @Order(11) + @DisplayName("测试11: 指令被拒绝场景 - canExecute返回false") + public void testCommandRejected() throws ExecutionException, InterruptedException { + log.info(">>> 场景:canExecute返回false,指令被拒绝"); + + Map params = new HashMap<>(); + params.put("canExecute", false); // 不允许执行 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.CANCEL_POINT, params); + + CommandResult result = future.get(); + assertFalse(result.isSuccess(), "指令应该被拒绝"); + assertTrue(result.getErrorMessage().contains("不能执行") || result.getErrorMessage().contains("拒绝") + || result.getErrorMessage().contains("cannot"), + "错误消息应该包含拒绝信息"); + log.info(">>> 测试通过:指令被拒绝,错误消息: {}", result.getErrorMessage()); + } + + /** + * 测试12: 指令被通过场景 + * canExecute返回true,指令可以执行 + */ + @Test + @Order(12) + @DisplayName("测试12: 指令被通过场景 - canExecute返回true") + public void testCommandAccepted() throws ExecutionException, InterruptedException { + log.info(">>> 场景:canExecute返回true,指令被通过"); + + Map params = new HashMap<>(); + params.put("canExecute", true); // 允许执行 + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.CANCEL_POINT, params); + + // 不发送任何回调,让它超时(因为这个指令没有配置回调) + // 但至少证明指令被接受并开始执行了 + + CommandResult result = future.get(); + // 这个指令没有配置回调,所以会立即成功 + assertTrue(result.isSuccess(), "指令应该被接受并执行"); + log.info(">>> 测试通过:指令被接受并执行"); + } + + /** + * 测试13: tid/bid 匹配成功场景 + * 回调消息中的 tid 和 bid 与指令执行时生成的值匹配 + */ + @Test + @Order(13) + @DisplayName("测试13: tid/bid匹配成功场景 - 回调消息tid/bid匹配") + public void testTidBidMatchSuccess() throws ExecutionException, InterruptedException { + log.info(">>> 场景:回调消息中的 tid 和 bid 与指令执行时生成的值匹配"); + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.EXIT_DRC_MODE, new HashMap<>()); + + // 需要获取生成的 tid 和 bid + // 注意:这里我们需要从 future 或其他方式获取 context 中的 tid/bid + // 为了测试,我们延迟一下,让指令开始执行,然后模拟正确的 tid/bid 响应 + + scheduler.schedule(() -> { + try { + // 在实际场景中,tid 和 bid 应该从指令执行上下文中获取 + // 这里为了演示,我们假设可以通过某种方式获取到 + // 实际使用时,设备会在收到命令后,将 tid/bid 原样返回 + + Thread.sleep(100); + + // 模拟方法回调 - 包含正确的 tid 和 bid + // 注意:在真实场景中,这些值应该从命令中获取并原样返回 + String response = "{\"tid\":\"test-tid-123\",\"bid\":\"test-bid-456\",\"data\":{\"result\":\"success\"}}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送方法回调(包含tid/bid): {}", response); + + Thread.sleep(100); + + // 模拟状态回调 - 包含正确的 tid 和 bid + response = "{\"tid\":\"test-tid-123\",\"bid\":\"test-bid-456\",\"status\":\"completed\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/state", response); + log.info(">>> 模拟发送状态回调(包含tid/bid): {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertTrue(result.isSuccess(), "指令应该执行成功(tid/bid匹配)"); + log.info(">>> 测试通过:tid/bid 匹配成功"); + } + + /** + * 测试14: tid/bid 不匹配场景 + * 回调消息中的 tid 或 bid 与指令执行时生成的值不匹配,应该超时 + */ + @Test + @Order(14) + @DisplayName("测试14: tid/bid不匹配场景 - 回调消息tid/bid不匹配导致超时") + public void testTidBidMismatch() throws ExecutionException, InterruptedException { + log.info(">>> 场景:回调消息中的 tid 或 bid 与指令执行时生成的值不匹配"); + + CompletableFuture future = + machineCommandManager.executeCommand(TEST_SN, CommandType.EXIT_DRC_MODE, new HashMap<>()); + + scheduler.schedule(() -> { + try { + Thread.sleep(100); + + // 模拟方法回调 - 包含错误的 tid 和 bid(不匹配) + String response = "{\"tid\":\"wrong-tid\",\"bid\":\"wrong-bid\",\"data\":{\"result\":\"success\"}}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/response", response); + log.info(">>> 模拟发送方法回调(tid/bid不匹配): {}", response); + + Thread.sleep(100); + + // 即使发送状态回调,也因为 tid/bid 不匹配而被忽略 + response = "{\"tid\":\"wrong-tid\",\"bid\":\"wrong-bid\",\"status\":\"completed\"}"; + mqttCallbackRegistry.handleMessage("test/" + TEST_SN + "/state", response); + log.info(">>> 模拟发送状态回调(tid/bid不匹配): {}", response); + + } catch (InterruptedException e) { + e.printStackTrace(); + } + }, 200, TimeUnit.MILLISECONDS); + + CommandResult result = future.get(); + assertFalse(result.isSuccess(), "指令应该因 tid/bid 不匹配而超时失败"); + assertTrue(result.getErrorMessage().contains("超时") || result.getErrorMessage().contains("timeout"), + "错误消息应该包含超时信息"); + log.info(">>> 测试通过:tid/bid 不匹配导致超时,错误消息: {}", result.getErrorMessage()); + } + + @AfterAll + public void cleanup() { + log.info("=== 综合测试清理 ==="); + scheduler.shutdown(); + log.info("=== 综合测试完成 ==="); + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/TestVendorConfig.java b/src/test/java/com/tuoheng/machine/vendor/test/TestVendorConfig.java new file mode 100644 index 0000000..6f99f2a --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/TestVendorConfig.java @@ -0,0 +1,158 @@ +package com.tuoheng.machine.vendor.test; + +import com.tuoheng.machine.command.CommandType; +import com.tuoheng.machine.command.Transaction; +import com.tuoheng.machine.state.*; +import com.tuoheng.machine.vendor.VendorConfig; +import com.tuoheng.machine.vendor.test.instruction.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 测试用厂家配置 + * 用于测试各种指令场景:成功/失败/超时/子命令等 + */ +@Slf4j +public class TestVendorConfig implements VendorConfig { + + private final Map transactionMap = new HashMap<>(); + + public TestVendorConfig() { + initTransactions(); + } + + @Override + public String getVendorType() { + return "TEST"; + } + + @Override + public String getVendorName() { + return "测试厂家"; + } + + @Override + public Transaction getTransaction(CommandType commandType) { + return transactionMap.get(commandType); + } + + @Override + public boolean canExecuteCommand(MachineStates currentStates, CommandType commandType) { + DroneState droneState = currentStates.getDroneState(); + AirportState airportState = currentStates.getAirportState(); + + switch (commandType) { + case TAKE_OFF: + return droneState == DroneState.ONLINE && airportState == AirportState.ONLINE; + case RETURN_HOME: + return droneState == DroneState.FLYING || droneState == DroneState.ARRIVED; + default: + return true; + } + } + + @Override + public List getAvailableCommands(MachineStates currentStates) { + List availableCommands = new ArrayList<>(); + for (CommandType commandType : CommandType.values()) { + if (canExecuteCommand(currentStates, commandType)) { + availableCommands.add(commandType); + } + } + return availableCommands; + } + + /** + * 初始化测试事务 + */ + private void initTransactions() { + // 1. 简单成功指令 - 指令被通过,远程命令成功,回复不超时 + Transaction simpleSuccessTransaction = new Transaction("简单成功指令", CommandType.TAKE_OFF) + .root(new TestSimpleSuccessInstruction()) + .setTimeout(10000); + transactionMap.put(CommandType.TAKE_OFF, simpleSuccessTransaction); + + // 2. 远程命令失败指令 - 指令被通过,但远程命令执行失败 + Transaction remoteFailTransaction = new Transaction("远程命令失败", CommandType.EMERGENCY_STOP) + .root(new TestRemoteFailInstruction()) + .setTimeout(10000); + transactionMap.put(CommandType.EMERGENCY_STOP, remoteFailTransaction); + + // 3. 方法回调超时指令 - 指令被通过,远程命令发送成功,但方法回调超时 + Transaction methodTimeoutTransaction = new Transaction("方法回调超时", CommandType.RESUME_FLIGHT) + .root(new TestMethodTimeoutInstruction()) + .setTimeout(10000); + transactionMap.put(CommandType.RESUME_FLIGHT, methodTimeoutTransaction); + + // 4. 状态回调超时指令 - 方法回调成功,但状态回调超时 + Transaction stateTimeoutTransaction = new Transaction("状态回调超时", CommandType.POINT_FLY) + .root(new TestStateTimeoutInstruction()) + .setTimeout(10000); + transactionMap.put(CommandType.POINT_FLY, stateTimeoutTransaction); + + // 5. 带成功子命令的指令 - 主指令成功后执行子指令 + TestCheckConditionInstruction checkInstruction = new TestCheckConditionInstruction(); + TestSuccessSubInstruction successSub = new TestSuccessSubInstruction(); + checkInstruction.onSuccess(successSub); + + Transaction successSubTransaction = new Transaction("带成功子命令", CommandType.OPEN_COVER) + .root(checkInstruction) + .setTimeout(15000); + transactionMap.put(CommandType.OPEN_COVER, successSubTransaction); + + // 6. 带失败子命令的指令 - 主指令失败后执行补救子指令 + TestCheckConditionInstruction checkInstruction2 = new TestCheckConditionInstruction(); + TestFailureSubInstruction failureSub = new TestFailureSubInstruction(); + TestRetryInstruction retryInstruction = new TestRetryInstruction(); + checkInstruction2.onFailure(failureSub.onSuccess(retryInstruction)); + + Transaction failureSubTransaction = new Transaction("带失败子命令", CommandType.CLOSE_COVER) + .root(checkInstruction2) + .setTimeout(20000); + transactionMap.put(CommandType.CLOSE_COVER, failureSubTransaction); + + // 7. 带always子命令的指令 - 无论成功失败都执行清理指令 + TestMainInstruction mainInstruction = new TestMainInstruction(); + TestCleanupInstruction cleanupInstruction = new TestCleanupInstruction(); + mainInstruction.then(cleanupInstruction); + + Transaction alwaysSubTransaction = new Transaction("带always子命令", CommandType.START_MISSION) + .root(mainInstruction) + .setTimeout(15000); + transactionMap.put(CommandType.START_MISSION, alwaysSubTransaction); + + // 8. 复杂指令树 - 包含成功/失败/always多层嵌套 + TestComplexRootInstruction complexRoot = new TestComplexRootInstruction(); + TestComplexSuccessInstruction complexSuccess = new TestComplexSuccessInstruction(); + TestComplexFailureInstruction complexFailure = new TestComplexFailureInstruction(); + TestComplexCleanupInstruction complexCleanup = new TestComplexCleanupInstruction(); + + complexRoot + .onSuccess(complexSuccess) + .onFailure(complexFailure) + .then(complexCleanup); + + Transaction complexTransaction = new Transaction("复杂指令树", CommandType.ENTER_DRC_MODE) + .root(complexRoot) + .setTimeout(25000); + transactionMap.put(CommandType.ENTER_DRC_MODE, complexTransaction); + + // 9. 指令被拒绝 - canExecute返回false + Transaction rejectedTransaction = new Transaction("指令被拒绝", CommandType.CANCEL_POINT) + .root(new TestRejectedInstruction()) + .setTimeout(10000); + transactionMap.put(CommandType.CANCEL_POINT, rejectedTransaction); + + // 10. tid/bid 匹配测试 - 测试回调消息的 tid/bid 匹配功能 + Transaction tidBidMatchTransaction = new Transaction("tid/bid匹配测试", CommandType.EXIT_DRC_MODE) + .root(new TestTidBidMatchInstruction()) + .setTimeout(10000); + transactionMap.put(CommandType.EXIT_DRC_MODE, tidBidMatchTransaction); + + log.info("测试厂家配置初始化完成,共配置{}个命令", transactionMap.size()); + } +} \ No newline at end of file diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCheckConditionInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCheckConditionInstruction.java new file mode 100644 index 0000000..8278150 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCheckConditionInstruction.java @@ -0,0 +1,59 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试条件检查指令 + * 场景:检查某个条件,根据结果执行不同的子指令 + * 可以通过context参数控制成功或失败 + */ +@Slf4j +public class TestCheckConditionInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_CHECK_CONDITION"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送条件检查指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"checkCondition\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 通过context参数控制返回成功或失败 + // 如果commandParams中有"shouldFail"=true,则期望失败的值 + Object shouldFail = context.getCommandParam("shouldFail"); + String expectedValue = (shouldFail != null && (Boolean) shouldFail) ? "fail" : "success"; + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue(expectedValue) + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; // 只需要方法回调 + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCleanupInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCleanupInstruction.java new file mode 100644 index 0000000..cb7c792 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestCleanupInstruction.java @@ -0,0 +1,53 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试清理指令(always子指令) + * 场景:无论主指令成功失败都会执行的清理操作 + */ +@Slf4j +public class TestCleanupInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_CLEANUP"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送清理指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"cleanup\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 清理指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("cleanupSuccess") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexCleanupInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexCleanupInstruction.java new file mode 100644 index 0000000..1c8f57e --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexCleanupInstruction.java @@ -0,0 +1,52 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试复杂指令树的清理指令(always分支) + */ +@Slf4j +public class TestComplexCleanupInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_COMPLEX_CLEANUP"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送复杂清理指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"complexCleanup\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 复杂清理指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("complexCleanup") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexFailureInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexFailureInstruction.java new file mode 100644 index 0000000..644ef81 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexFailureInstruction.java @@ -0,0 +1,52 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试复杂指令树的失败分支指令 + */ +@Slf4j +public class TestComplexFailureInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_COMPLEX_FAILURE"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送复杂失败分支指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"complexFailure\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 复杂失败分支指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("complexFailure") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexRootInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexRootInstruction.java new file mode 100644 index 0000000..8b5df0a --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexRootInstruction.java @@ -0,0 +1,57 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试复杂指令树的根指令 + * 场景:复杂的多层嵌套指令树,包含成功/失败/always分支 + */ +@Slf4j +public class TestComplexRootInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_COMPLEX_ROOT"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送复杂根指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"complexRoot\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 复杂根指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 通过context参数控制返回成功或失败 + Object shouldFail = context.getCommandParam("complexRootShouldFail"); + String expectedValue = (shouldFail != null && (Boolean) shouldFail) ? "fail" : "success"; + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue(expectedValue) + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexSuccessInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexSuccessInstruction.java new file mode 100644 index 0000000..4bc9098 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestComplexSuccessInstruction.java @@ -0,0 +1,52 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试复杂指令树的成功分支指令 + */ +@Slf4j +public class TestComplexSuccessInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_COMPLEX_SUCCESS"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送复杂成功分支指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"complexSuccess\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 复杂成功分支指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("complexSuccess") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestFailureSubInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestFailureSubInstruction.java new file mode 100644 index 0000000..631a52b --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestFailureSubInstruction.java @@ -0,0 +1,53 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试失败分支子指令 + * 场景:当父指令失败时执行的补救子指令 + */ +@Slf4j +public class TestFailureSubInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_FAILURE_SUB"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送失败补救子指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"failureSub\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 失败补救子指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("remedySuccess") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMainInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMainInstruction.java new file mode 100644 index 0000000..402aa6c --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMainInstruction.java @@ -0,0 +1,58 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试主指令(带always子指令) + * 场景:无论成功失败都会执行清理指令 + * 可以通过context参数控制成功或失败 + */ +@Slf4j +public class TestMainInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_MAIN"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送主指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"main\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 主指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 通过context参数控制返回成功或失败 + Object shouldFail = context.getCommandParam("mainShouldFail"); + String expectedValue = (shouldFail != null && (Boolean) shouldFail) ? "fail" : "success"; + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue(expectedValue) + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMethodTimeoutInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMethodTimeoutInstruction.java new file mode 100644 index 0000000..7d61457 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestMethodTimeoutInstruction.java @@ -0,0 +1,54 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试方法回调超时指令 + * 场景:远程命令发送成功,但方法回调超时(测试中不发送回调消息) + */ +@Slf4j +public class TestMethodTimeoutInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_METHOD_TIMEOUT"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送方法超时指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"methodTimeout\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] MQTT发送成功,但不会收到方法回调: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 设置较短的超时时间,测试中不发送回调消息,会超时 + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("success") + .timeoutMs(2000) // 2秒超时 + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; // 方法回调就超时了,不会执行状态回调 + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRejectedInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRejectedInstruction.java new file mode 100644 index 0000000..2232dd3 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRejectedInstruction.java @@ -0,0 +1,55 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试被拒绝的指令 + * 场景:canExecute返回false,指令不会被执行 + */ +@Slf4j +public class TestRejectedInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_REJECTED"; + } + + @Override + public boolean canExecute(InstructionContext context) { + // 通过context参数控制是否可以执行 + Object canExecute = context.getCommandParam("canExecute"); + boolean result = canExecute != null && (Boolean) canExecute; + log.info("[测试] 检查指令是否可执行: {}", result); + return result; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送被拒绝指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"rejected\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 被拒绝指令MQTT发送成功: topic={}, payload=", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRemoteFailInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRemoteFailInstruction.java new file mode 100644 index 0000000..44e141f --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRemoteFailInstruction.java @@ -0,0 +1,43 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试远程命令失败指令 + * 场景:指令被通过,但远程命令执行时抛出异常 + */ +@Slf4j +public class TestRemoteFailInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_REMOTE_FAIL"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送远程失败指令: sn={}", sn); + + // 模拟远程调用失败 + throw new RuntimeException("模拟远程命令执行失败"); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + return null; // 远程调用就失败了,不需要回调 + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRetryInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRetryInstruction.java new file mode 100644 index 0000000..32b604c --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestRetryInstruction.java @@ -0,0 +1,53 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试重试指令 + * 场景:补救指令成功后的重试操作 + */ +@Slf4j +public class TestRetryInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_RETRY"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送重试指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"retry\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 重试指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("retrySuccess") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSimpleSuccessInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSimpleSuccessInstruction.java new file mode 100644 index 0000000..da616ad --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSimpleSuccessInstruction.java @@ -0,0 +1,60 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试简单成功指令 + * 场景:指令被通过,远程命令成功,方法回调和状态回调都成功 + */ +@Slf4j +public class TestSimpleSuccessInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_SIMPLE_SUCCESS"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送简单成功指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"simpleSuccess\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("success") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/state") + .fieldPath("status") + .expectedValue("completed") + .timeoutMs(5000) + .build(); + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestStateTimeoutInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestStateTimeoutInstruction.java new file mode 100644 index 0000000..41e73da --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestStateTimeoutInstruction.java @@ -0,0 +1,62 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试状态回调超时指令 + * 场景:方法回调成功,但状态回调超时 + */ +@Slf4j +public class TestStateTimeoutInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_STATE_TIMEOUT"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送状态超时指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"stateTimeout\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 方法回调会成功(测试中会发送此消息) + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("success") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 状态回调会超时(测试中不发送此消息) + return CallbackConfig.builder() + .topic("test/" + sn + "/state") + .fieldPath("status") + .expectedValue("completed") + .timeoutMs(2000) // 2秒超时 + .build(); + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSuccessSubInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSuccessSubInstruction.java new file mode 100644 index 0000000..d696153 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestSuccessSubInstruction.java @@ -0,0 +1,53 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试成功分支子指令 + * 场景:当父指令成功时执行的子指令 + */ +@Slf4j +public class TestSuccessSubInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_SUCCESS_SUB"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + log.info("[测试] 发送成功子指令: sn={}", sn); + + String topic = "test/" + sn + "/command"; + String payload = "{\"cmd\":\"successSub\"}"; + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] 成功子指令MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("result") + .expectedValue("subSuccess") + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + return null; + } + + @Override + public long getTimeoutMs() { + return 10000; + } +} diff --git a/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestTidBidMatchInstruction.java b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestTidBidMatchInstruction.java new file mode 100644 index 0000000..7278940 --- /dev/null +++ b/src/test/java/com/tuoheng/machine/vendor/test/instruction/TestTidBidMatchInstruction.java @@ -0,0 +1,78 @@ +package com.tuoheng.machine.vendor.test.instruction; + +import com.tuoheng.machine.instruction.AbstractInstruction; +import com.tuoheng.machine.instruction.CallbackConfig; +import com.tuoheng.machine.instruction.InstructionContext; +import lombok.extern.slf4j.Slf4j; + +/** + * 测试 tid/bid 匹配的指令 + * 场景:只有当回调消息中的 tid 和 bid 与指令执行时生成的值匹配时,才会触发回调 + */ +@Slf4j +public class TestTidBidMatchInstruction extends AbstractInstruction { + + @Override + public String getName() { + return "TEST_TID_BID_MATCH"; + } + + @Override + public void executeRemoteCall(InstructionContext context) throws Exception { + String sn = context.getSn(); + String tid = context.getTid(); + String bid = context.getBid(); + + log.info("[测试] 发送 tid/bid 匹配指令: sn={}, tid={}, bid={}", sn, tid, bid); + + String topic = "test/" + sn + "/command"; + // 在实际发送的消息中包含 tid 和 bid + String payload = String.format("{\"cmd\":\"tidBidTest\",\"tid\":\"%s\",\"bid\":\"%s\"}", tid, bid); + + context.getMqttClient().sendMessage(topic, payload); + log.debug("[测试] MQTT发送成功: topic={}, payload={}", topic, payload); + } + + @Override + public CallbackConfig getMethodCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 配置方法回调,要求 tid 和 bid 必须匹配 + return CallbackConfig.builder() + .topic("test/" + sn + "/response") + .fieldPath("data.result") + .expectedValue("success") + // 配置 tid 匹配 + .tidFieldPath("tid") + .expectedTid(context.getTid()) + // 配置 bid 匹配 + .bidFieldPath("bid") + .expectedBid(context.getBid()) + .timeoutMs(5000) + .build(); + } + + @Override + public CallbackConfig getStateCallbackConfig(InstructionContext context) { + String sn = context.getSn(); + + // 配置状态回调,同样要求 tid 和 bid 必须匹配 + return CallbackConfig.builder() + .topic("test/" + sn + "/state") + .fieldPath("status") + .expectedValue("completed") + // 配置 tid 匹配 + .tidFieldPath("tid") + .expectedTid(context.getTid()) + // 配置 bid 匹配 + .bidFieldPath("bid") + .expectedBid(context.getBid()) + .timeoutMs(5000) + .build(); + } + + @Override + public long getTimeoutMs() { + return 10000; + } +}