#include #include #include #include #include #include "goal_source_adapter.h" #include "mission_test_utils.h" #include "vda5050_source_adapter.h" namespace { using namespace mission_adapters; using mission_plugins::GoalSourceAdapter; using mission_plugins::VDA5050SourceAdapter; using mission_test::makeAction; using mission_test::makeGoal; using mission_test::makeOrder; class AdapterTest : public ::testing::Test { protected: void SetUp() override { robot::NodeHandle nh; ASSERT_TRUE(goal_adapter.configure("goal_src", nh)); ASSERT_TRUE(order_adapter.configure("vda5050_src", nh)); } /// Chuyển một order, trả về danh sách mission (bỏ phần mode). std::vector> convertOrder(const robot_protocol_msgs::Order& order) { return order_adapter.convert(MissionRequest::fromOrder(order)).missions; } /// Chuyển một order và giữ nguyên cả ConversionResult để kiểm mode. ConversionResult convertOrderFull(const robot_protocol_msgs::Order& order) { return order_adapter.convert(MissionRequest::fromOrder(order)); } GoalSourceAdapter goal_adapter; VDA5050SourceAdapter order_adapter; }; // ── Schema ────────────────────────────────────────────────────────────────────────────────────── TEST_F(AdapterTest, AdaptersDeclareDistinctSchemas) { EXPECT_EQ(goal_adapter.schema(), schema::kPoseStamped); EXPECT_EQ(order_adapter.schema(), schema::kVda5050Order); EXPECT_NE(goal_adapter.schema(), order_adapter.schema()); } // ── GoalSourceAdapter ─────────────────────────────────────────────────────────────────────────── TEST_F(AdapterTest, GoalAdapterCreatesSingleMission) { const auto missions = goal_adapter.convert(MissionRequest::fromPose(makeGoal(5.5, 9.1))).missions; ASSERT_EQ(missions.size(), 1u); EXPECT_EQ(missions.front()->type, MissionType::SIMPLE_GOAL); EXPECT_EQ(missions.front()->motion_hint, "position"); EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.x, 5.5); EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.y, 9.1); } TEST_F(AdapterTest, GoalAdapterRejectsMissingPayload) { MissionRequest request; request.schema = schema::kPoseStamped; std::string reason; EXPECT_FALSE(goal_adapter.validate(request, reason)); EXPECT_FALSE(reason.empty()); } TEST_F(AdapterTest, GoalAdapterRejectsNonFiniteGoal) { auto goal = makeGoal(1.0, 1.0); goal.pose.position.x = std::numeric_limits::quiet_NaN(); goal.pose.orientation.w = 1.0; std::string reason; EXPECT_FALSE(goal_adapter.validate(MissionRequest::fromPose(goal), reason)) << "a goal containing NaN slipped through the boundary into navigation"; } TEST_F(AdapterTest, GoalAdapterRejectsZeroQuaternion) { auto goal = makeGoal(1.0, 1.0); std::string reason; ASSERT_TRUE(goal_adapter.validate(MissionRequest::fromPose(goal), reason)) << reason; // Host quên set orientation: quaternion toàn 0 không phải "hướng bất kỳ", nó là dữ liệu hỏng. goal.pose.orientation.w = 0.0; EXPECT_FALSE(goal_adapter.validate(MissionRequest::fromPose(goal), reason)); } // ── VDA5050SourceAdapter ──────────────────────────────────────────────────────────────────────── TEST_F(AdapterTest, EmptyOrderIsRejectedByValidate) { robot_protocol_msgs::Order order; std::string reason; EXPECT_FALSE(order_adapter.validate(MissionRequest::fromOrder(order), reason)); EXPECT_TRUE(convertOrder(order).empty()); } TEST_F(AdapterTest, OrderWithoutActionsCreatesOneTailMission) { const auto missions = convertOrder(makeOrder(4)); ASSERT_EQ(missions.size(), 1u); EXPECT_EQ(missions.front()->nodes.size(), 4u); EXPECT_EQ(missions.front()->edges.size(), 3u); } TEST_F(AdapterTest, InvalidOrderWithMissingEdgesIsRejected) { auto order = makeOrder(4); order.edges.pop_back(); std::string reason; EXPECT_FALSE(order_adapter.validate(MissionRequest::fromOrder(order), reason)); EXPECT_TRUE(convertOrder(order).empty()); } TEST_F(AdapterTest, OrderSplitsAtNodeAction) { auto order = makeOrder(5); order.nodes[2].actions.push_back(makeAction("dock")); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 2u); EXPECT_EQ(missions[0]->nodes.size(), 3u); EXPECT_EQ(missions[1]->nodes.size(), 3u); } TEST_F(AdapterTest, OrderCollectsAndSortsActions) { auto order = makeOrder(2); order.edges[0].actions.push_back(makeAction("edge")); order.nodes[1].actions.push_back(makeAction("node")); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u); ASSERT_EQ(missions.front()->actions.size(), 2u); EXPECT_EQ(missions.front()->actions[0].type, ActionType::EDGE_ACTION); EXPECT_EQ(missions.front()->actions[1].type, ActionType::NODE_ACTION); } // ── A4: conformance VDA5050 ───────────────────────────────────────────────────────────────────── TEST_F(AdapterTest, MissionCarriesGoalAndStartFromNodes) { auto order = makeOrder(3); order.nodes[2].nodePosition.x = 7.0; order.nodes[2].nodePosition.y = 8.0; order.nodes[2].nodePosition.theta = 0.0; const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u); // Mission phải self-contained: consumer không phải tự đoán goal từ nodes.back(). EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.x, 7.0); EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.y, 8.0); EXPECT_DOUBLE_EQ(missions.front()->goal.pose.orientation.w, 1.0); EXPECT_EQ(missions.front()->goal.header.frame_id, "map"); EXPECT_DOUBLE_EQ(missions.front()->start.pose.position.x, 0.0); } TEST_F(AdapterTest, NodeThetaBecomesQuaternion) { auto order = makeOrder(2); order.nodes[1].nodePosition.theta = M_PI; // [rad] const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u); // theta = pi -> quay 180 độ quanh z: (z, w) = (1, 0). EXPECT_NEAR(missions.front()->goal.pose.orientation.z, 1.0, 1e-9); EXPECT_NEAR(missions.front()->goal.pose.orientation.w, 0.0, 1e-9); } TEST_F(AdapterTest, HorizonNodesAreNeverDispatched) { auto order = makeOrder(5); // Chỉ 3 node đầu là base; hai node cuối là horizon (fleet manager chưa cho phép đi). order.nodes[3].released = false; order.nodes[4].released = false; order.edges[3].released = false; const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u); EXPECT_EQ(missions.front()->nodes.size(), 3u) << "the robot was handed the horizon part that " "is not released yet"; EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.x, 2.0); } TEST_F(AdapterTest, OrderWithoutReleasedFlagIsTreatedAsFullBase) { auto order = makeOrder(3); for (auto& node : order.nodes) node.released = false; for (auto& edge : order.edges) edge.released = false; // Host không điền `released`: chạy cả order còn hơn đứng im không dấu hiệu. const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u); EXPECT_EQ(missions.front()->nodes.size(), 3u); } TEST_F(AdapterTest, StaleOrderUpdateIdIsRejected) { ASSERT_FALSE(convertOrder(makeOrder(3, "order_A", 2)).empty()); std::string reason; // Cùng orderId, orderUpdateId cũ hơn -> phát lại/đến trễ, không được chạy lại tuyến đường. EXPECT_FALSE(order_adapter.validate(MissionRequest::fromOrder(makeOrder(3, "order_A", 1)), reason)); EXPECT_FALSE(order_adapter.validate(MissionRequest::fromOrder(makeOrder(3, "order_A", 2)), reason)); // Cùng orderId, update mới hơn -> chấp nhận. EXPECT_TRUE(order_adapter.validate(MissionRequest::fromOrder(makeOrder(4, "order_A", 3)), reason)) << reason; // orderId khác -> order mới, không so orderUpdateId. EXPECT_TRUE(order_adapter.validate(MissionRequest::fromOrder(makeOrder(3, "order_B", 0)), reason)) << reason; } TEST_F(AdapterTest, OrderUpdateAppendsOnlyNewlyReleasedSegment) { // Base ban đầu: 3 node released, 2 node horizon. auto order = makeOrder(5, "order_A", 0); order.nodes[3].released = false; order.nodes[4].released = false; order.edges[3].released = false; const auto first = convertOrderFull(order); ASSERT_EQ(first.missions.size(), 1u); EXPECT_EQ(first.mode, SubmitMode::kReplace); EXPECT_EQ(first.missions.front()->nodes.size(), 3u); // Fleet manager release nốt horizon. auto update = makeOrder(5, "order_A", 1); const auto second = convertOrderFull(update); ASSERT_EQ(second.missions.size(), 1u); EXPECT_EQ(second.mode, SubmitMode::kAppend) << "an order update replaced the whole queue -> the robot cancels and redoes the leg it is " "on"; // Chặng mới bắt đầu từ node cuối của phần đã chạy, không lặp lại phần cũ. EXPECT_EQ(second.missions.front()->nodes.size(), 3u); EXPECT_DOUBLE_EQ(second.missions.front()->start.pose.position.x, 2.0); EXPECT_DOUBLE_EQ(second.missions.front()->goal.pose.position.x, 4.0); } TEST_F(AdapterTest, OrderUpdateWithoutNewNodesProducesNoWork) { ASSERT_FALSE(convertOrder(makeOrder(3, "order_A", 0)).empty()); // Update không release thêm node nào: không có việc mới, và cũng không được xoá gì. const auto result = convertOrderFull(makeOrder(3, "order_A", 1)); EXPECT_TRUE(result.empty()); } TEST_F(AdapterTest, NewOrderIdReplacesQueue) { ASSERT_FALSE(convertOrder(makeOrder(3, "order_A", 0)).empty()); const auto result = convertOrderFull(makeOrder(2, "order_B", 0)); ASSERT_FALSE(result.empty()); EXPECT_EQ(result.mode, SubmitMode::kReplace); } // ── D8: goal optional, action đi nguyên vẹn ───────────────────────────────────────────────────── TEST_F(AdapterTest, ActionAtFirstNodeBecomesGoallessMission) { auto order = makeOrder(3); order.nodes[0].actions.push_back(makeAction("pick_at_start")); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 2u); // Chặng đầu: action ngay tại chỗ đứng, không có quãng đường nào để đi. EXPECT_FALSE(missions[0]->has_goal); ASSERT_EQ(missions[0]->actions.size(), 1u); EXPECT_EQ(missions[0]->actions.front().action.actionId, "pick_at_start"); // Chặng sau: di chuyển bình thường. EXPECT_TRUE(missions[1]->has_goal); EXPECT_DOUBLE_EQ(missions[1]->goal.pose.position.x, 2.0); } TEST_F(AdapterTest, MovingMissionsAlwaysHaveGoal) { const auto missions = convertOrder(makeOrder(3)); ASSERT_EQ(missions.size(), 1u); EXPECT_TRUE(missions.front()->has_goal); } TEST_F(AdapterTest, ActionsPassThroughUnchangedAndInOrder) { auto order = makeOrder(3); order.edges[0].sequenceId = 1; order.edges[0].actions.push_back(makeAction("beep")); order.edges[1].sequenceId = 3; order.edges[1].actions.push_back(makeAction("horn")); order.nodes[2].sequenceId = 4; order.nodes[2].actions.push_back(makeAction("lift")); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u); const auto& actions = missions.front()->actions; ASSERT_EQ(actions.size(), 3u); EXPECT_EQ(actions[0].action.actionId, "beep"); EXPECT_EQ(actions[1].action.actionId, "horn"); EXPECT_EQ(actions[2].action.actionId, "lift"); // Mission layer không diễn giải actionType — nó chỉ chuyển tiếp. EXPECT_EQ(actions[2].action.actionType, "TEST"); EXPECT_EQ(actions[2].type, ActionType::NODE_ACTION); } TEST_F(AdapterTest, NonFiniteNodePositionIsRejected) { auto order = makeOrder(3); order.nodes[1].nodePosition.x = std::numeric_limits::infinity(); std::string reason; EXPECT_FALSE(order_adapter.validate(MissionRequest::fromOrder(order), reason)); } } // namespace // ── Compound action: mở rộng thành chuỗi chặng ────────────────────────────────────────────────── namespace { /// @brief Gắn một action có tham số vào node. void addAction(robot_protocol_msgs::Node& node, const std::string& type, const std::string& param_key = "", const std::string& param_value = "") { robot_protocol_msgs::Action action; action.actionType = type; action.actionId = type + "_id"; if (!param_key.empty()) { robot_protocol_msgs::ActionParameter p; p.key = param_key; p.value = param_value; action.actionParameters.push_back(p); } node.actions.push_back(action); } } // namespace TEST_F(AdapterTest, CompoundActionExpandsIntoASequenceOfLegs) { auto order = makeOrder(2); addAction(order.nodes[1], "charge", "goal_frame", "charger_02_goal"); const auto missions = convertOrder(order); // nav -> n1 | DetectCharger | docking | startCharging ASSERT_EQ(missions.size(), 4u); EXPECT_TRUE(missions[0]->has_goal); EXPECT_TRUE(missions[0]->actions.empty()) << "action compound phải được GỠ khỏi chặng nav"; EXPECT_EQ(missions[0]->motion_hint, "position"); ASSERT_EQ(missions[1]->actions.size(), 1u); EXPECT_EQ(missions[1]->actions[0].action.actionType, "DetectCharger"); EXPECT_FALSE(missions[1]->has_goal); EXPECT_TRUE(missions[2]->has_goal); EXPECT_EQ(missions[2]->motion_hint, "docking"); EXPECT_EQ(missions[2]->goal_frame, "charger_02_goal") << "frame phải lấy từ actionParameters"; EXPECT_EQ(missions[2]->marker, "charger") << "marker phải chọn override planner độc lập TF"; ASSERT_EQ(missions[3]->actions.size(), 1u); EXPECT_EQ(missions[3]->actions[0].action.actionType, "startCharging"); EXPECT_FALSE(missions[3]->has_goal); } TEST_F(AdapterTest, GeneratedActionsCarryTheOriginalParameters) { auto order = makeOrder(2); addAction(order.nodes[1], "charge", "goal_frame", "charger_02_goal"); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 4u); // Handler dò cần biết dò trạm nào; nó là chỗ duy nhất hiểu ý nghĩa các tham số đó. const auto& detect = missions[1]->actions[0].action; ASSERT_EQ(detect.actionParameters.size(), 1u); EXPECT_EQ(detect.actionParameters[0].key, "goal_frame"); EXPECT_EQ(detect.actionParameters[0].value, "charger_02_goal"); // actionId suy từ id gốc để truy vết được trong log. EXPECT_NE(detect.actionId.find("charge_id"), std::string::npos); } TEST_F(AdapterTest, PlainActionsKeepTheirJsonOrderAroundACompound) { auto order = makeOrder(2); addAction(order.nodes[1], "MutedOn"); addAction(order.nodes[1], "charge", "goal_frame", "charger_goal"); addAction(order.nodes[1], "MutedOff"); const auto missions = convertOrder(order); // nav+MutedOn | Detect | docking | startCharging | MutedOff ASSERT_EQ(missions.size(), 5u); ASSERT_EQ(missions[0]->actions.size(), 1u); EXPECT_EQ(missions[0]->actions[0].action.actionType, "MutedOn"); // MutedOff phải nằm SAU chuỗi charge. Gom hết action thường vào chặng nav sẽ bật lại cảm biến // an toàn trước khi robot lùi vào trạm — đúng thứ MutedOn sinh ra để tránh. ASSERT_EQ(missions[4]->actions.size(), 1u); EXPECT_EQ(missions[4]->actions[0].action.actionType, "MutedOff"); EXPECT_FALSE(missions[4]->has_goal); } TEST_F(AdapterTest, ActionOutsideTheTableRunsAsAPlainAction) { auto order = makeOrder(2); addAction(order.nodes[1], "MutedOn"); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 1u) << "action thường không được mở rộng"; ASSERT_EQ(missions[0]->actions.size(), 1u); EXPECT_EQ(missions[0]->actions[0].action.actionType, "MutedOn"); } TEST_F(AdapterTest, RelativeMoveStepBecomesALegWithADistance) { auto order = makeOrder(2); addAction(order.nodes[1], "UnDockFromStation"); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 2u); EXPECT_TRUE(missions[1]->has_goal); EXPECT_EQ(missions[1]->motion_hint, "go_straight"); EXPECT_DOUBLE_EQ(missions[1]->relative_distance, -1.0); EXPECT_TRUE(missions[1]->goal_frame.empty()); } TEST_F(AdapterTest, FixedFrameStepDoesNotNeedAnyParameter) { auto order = makeOrder(2); addAction(order.nodes[1], "FixedDock"); const auto missions = convertOrder(order); ASSERT_EQ(missions.size(), 2u); EXPECT_EQ(missions[1]->goal_frame, "dock_target"); EXPECT_EQ(missions[1]->marker, "trolley"); } TEST_F(AdapterTest, MissingStructuralParameterDropsTheWholeOrder) { auto order = makeOrder(2); addAction(order.nodes[1], "charge"); // thiếu goal_frame // Bỏ CẢ order: một order chạy nửa vời — robot tới trạm sạc rồi không sạc — nguy hiểm hơn là // không chạy. Và hàng đợi đang chạy không bị đụng tới (A1). EXPECT_TRUE(convertOrder(order).empty()); } TEST(CompoundConfig, RejectsAStepWithTwoKeywords) { robot::NodeHandle nh; VDA5050SourceAdapter adapter; EXPECT_FALSE(adapter.configure("vda5050_bad_step", nh)) << "không đúng một từ khoá thì không có cách diễn giải nào hiển nhiên đúng"; } TEST(CompoundConfig, RejectsARecursiveCompound) { robot::NodeHandle nh; VDA5050SourceAdapter adapter; EXPECT_FALSE(adapter.configure("vda5050_recursive", nh)) << "expander sẽ mở rộng chính output của mình"; } TEST(CompoundConfig, RejectsInvalidOrActionOnlyProfiles) { robot::NodeHandle nh; VDA5050SourceAdapter adapter; EXPECT_FALSE(adapter.configure("vda5050_bad_profile", nh)); } int main(int argc, char** argv) { // Bảng `compound_actions` nằm trong cây config test. `overwrite = 0` để shell vẫn override được // khi cần chạy tay với cây khác — cùng cách `plugin_registry_test` làm. #ifdef MISSION_ADAPTERS_TEST_CONFIG_DIR setenv("PNKX_NAV_CORE_CONFIG_DIR", MISSION_ADAPTERS_TEST_CONFIG_DIR, 0); #endif testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }