/********************************************************************* * * Kiểm đường nạp plugin thật: YAML -> library_path -> Boost.DLL -> schema. * * Test này cần PNKX_NAV_CORE_CONFIG_DIR trỏ vào test/config của gói: * * PNKX_NAV_CORE_CONFIG_DIR=src/AMR_T800/Test/mission_adapters/test/config \ * ./devel/lib/mission_adapters/plugin_registry_test * *********************************************************************/ #include #include #include #include #include #include #include #include #include "mission_test_utils.h" namespace { using namespace mission_adapters; /** * @brief Nguồn mission thứ ba, viết hoàn toàn trong test. * * Sự tồn tại của nó là bài kiểm tra thật cho tính plugin: thêm một loại nguồn mới mà không sửa file * nào trong `src/` của gói. */ class DummySourceAdapter : public MissionSourceAdapter { public: static constexpr const char* kSchema = "test.dummy"; bool configure(const std::string& name, robot::NodeHandle& nh) override { (void)nh; name_ = name; return true; } std::string schema() const override { return kSchema; } bool validate(const MissionRequest& request, std::string& reason) const override { if (request.raw_payload.empty()) { reason = "raw_payload is empty"; return false; } return true; } ConversionResult convert(const MissionRequest& request) override { (void)request; auto mission = std::make_shared(); mission->type = MissionType::SIMPLE_GOAL; ConversionResult result; result.missions.push_back(mission); return result; } private: std::string name_; }; class PluginRegistryTest : public ::testing::Test { protected: robot::NodeHandle nh; }; // ── Đường nạp bình thường ──────────────────────────────────────────────────────────────────────── TEST_F(PluginRegistryTest, LoadsBothSourcesFromConfig) { PluginRegistry registry; ASSERT_TRUE(registry.loadFromConfig(nh)) << "could not load the plugin — check PNKX_NAV_CORE_CONFIG_DIR and devel/lib"; EXPECT_EQ(registry.size(), 2u); EXPECT_NE(registry.find(schema::kPoseStamped), nullptr); EXPECT_NE(registry.find(schema::kVda5050Order), nullptr); EXPECT_EQ(registry.find("does.not.exist"), nullptr); } TEST_F(PluginRegistryTest, LoadedAdapterConvertsRealPayload) { PluginRegistry registry; ASSERT_TRUE(registry.loadFromConfig(nh)); auto* adapter = registry.find(schema::kPoseStamped); ASSERT_NE(adapter, nullptr); const auto request = MissionRequest::fromPose(mission_test::makeGoal(3.0, 4.0)); std::string reason; ASSERT_TRUE(adapter->validate(request, reason)) << reason; const auto result = adapter->convert(request); ASSERT_EQ(result.missions.size(), 1u); EXPECT_DOUBLE_EQ(result.missions.front()->goal.pose.position.x, 3.0); } // ── Các cách hỏng, đều phải báo lỗi rõ ràng chứ không crash ────────────────────────────────────── TEST_F(PluginRegistryTest, MissingLibraryPathKeyFailsCleanly) { PluginRegistry registry; EXPECT_FALSE(registry.loadFromConfig(nh, "registry_test_missing_library_path")); EXPECT_EQ(registry.size(), 0u); } TEST_F(PluginRegistryTest, MissingLibraryFileFailsCleanly) { PluginRegistry registry; EXPECT_FALSE(registry.loadFromConfig(nh, "registry_test_missing_library_file")); EXPECT_EQ(registry.size(), 0u); } TEST_F(PluginRegistryTest, WrongSymbolNameFailsCleanly) { PluginRegistry registry; // Thư viện có thật, symbol thì không: import_alias ném system_error, registry phải nuốt và báo. EXPECT_FALSE(registry.loadFromConfig(nh, "registry_test_wrong_symbol")); EXPECT_EQ(registry.size(), 0u); } TEST_F(PluginRegistryTest, DuplicateSchemaIsRejected) { PluginRegistry registry; // Hai instance cùng khai schema "geometry.pose_stamped": cái thứ hai bị từ chối thay vì ghi đè // im lặng, vì định tuyến khi đó sẽ phụ thuộc thứ tự nạp. EXPECT_FALSE(registry.loadFromConfig(nh, "registry_test_duplicate_schema")); EXPECT_EQ(registry.size(), 1u); } TEST_F(PluginRegistryTest, EntryWithoutTypeFailsCleanly) { PluginRegistry registry; EXPECT_FALSE(registry.loadFromConfig(nh, "registry_test_entry_without_type")); EXPECT_EQ(registry.size(), 0u); } TEST_F(PluginRegistryTest, MissingSourceListFailsCleanly) { PluginRegistry registry; EXPECT_FALSE(registry.loadFromConfig(nh, "registry_test_empty")); EXPECT_EQ(registry.size(), 0u); } // ── Đăng ký trực tiếp (nguồn biên dịch thẳng vào host, hoặc test) ─────────────────────────────── TEST_F(PluginRegistryTest, RejectsNullAndDuplicateManualRegistration) { PluginRegistry registry; EXPECT_FALSE(registry.registerAdapter(nullptr)); auto first = std::make_shared(); EXPECT_TRUE(registry.registerAdapter(first)); auto second = std::make_shared(); EXPECT_FALSE(registry.registerAdapter(second)) << "two adapters with the same schema were both " "accepted"; EXPECT_EQ(registry.size(), 1u); } /** * Nguồn thứ ba đi hết đường: đăng ký -> EventProcessor định tuyến theo schema -> mission vào hàng * đợi. Không có dòng nào trong `src/` của gói biết tới DummySourceAdapter. */ TEST_F(PluginRegistryTest, ThirdPartyAdapterFlowsThroughCoreUnchanged) { PluginRegistry registry; ASSERT_TRUE(registry.registerAdapter(std::make_shared())); MissionManager manager; EventProcessor processor(manager, registry); processor.start(); processor.submitRequest( MissionRequest::fromRaw(DummySourceAdapter::kSchema, "{\"job\":\"pick\"}")); EXPECT_TRUE(mission_test::waitForState(manager, MissionState::QUEUED, std::chrono::milliseconds(500))); // Payload không hợp lệ -> validate() chặn -> hàng đợi không đổi (A1). processor.submitRequest(MissionRequest::fromRaw(DummySourceAdapter::kSchema, "")); std::this_thread::sleep_for(std::chrono::milliseconds(80)); EXPECT_EQ(manager.state(), MissionState::QUEUED); EXPECT_TRUE(manager.hasMission()); processor.stop(); } /** * @brief Plugin hỏng: sinh mission không goal và cũng không action. * * Core phải chặn, vì chặng như vậy không có việc gì để làm và sẽ không bao giờ báo kết quả về — * mission layer kẹt RUNNING vĩnh viễn. */ class EmptyMissionAdapter : public MissionSourceAdapter { public: static constexpr const char* kSchema = "test.empty_mission"; bool configure(const std::string&, robot::NodeHandle&) override { return true; } std::string schema() const override { return kSchema; } bool validate(const MissionRequest&, std::string&) const override { return true; } ConversionResult convert(const MissionRequest&) override { auto mission = std::make_shared(); mission->has_goal = false; // và actions rỗng ConversionResult result; result.missions.push_back(mission); return result; } }; TEST_F(PluginRegistryTest, MissionWithoutGoalAndWithoutActionIsRejected) { PluginRegistry registry; ASSERT_TRUE(registry.registerAdapter(std::make_shared())); MissionManager manager; EventProcessor processor(manager, registry); processor.start(); processor.submitRequest(MissionRequest::fromRaw(EmptyMissionAdapter::kSchema, "payload")); std::this_thread::sleep_for(std::chrono::milliseconds(80)); processor.stop(); EXPECT_EQ(manager.state(), MissionState::IDLE); EXPECT_FALSE(manager.hasMission()); } TEST_F(PluginRegistryTest, UnknownSchemaIsIgnoredNotCrashing) { PluginRegistry registry; MissionManager manager; EventProcessor processor(manager, registry); processor.start(); processor.submitRequest(MissionRequest::fromRaw("nobody.handles.this", "payload")); std::this_thread::sleep_for(std::chrono::milliseconds(80)); processor.stop(); EXPECT_EQ(manager.state(), MissionState::IDLE); EXPECT_FALSE(manager.hasMission()); } } // namespace int main(int argc, char** argv) { // Test tự trỏ vào cây config và thư mục .so của chính nó, để chạy được cả qua ctest lẫn khi gọi // thẳng binary (đúng cách test_costmap đang làm). overwrite = 0 nên biến môi trường do người // chạy đặt vẫn thắng. #ifdef MISSION_ADAPTERS_TEST_CONFIG_DIR setenv("PNKX_NAV_CORE_CONFIG_DIR", MISSION_ADAPTERS_TEST_CONFIG_DIR, 0); #endif #ifdef MISSION_ADAPTERS_TEST_LIBRARY_DIR setenv("PNKX_NAV_CORE_LIBRARY_PATH", MISSION_ADAPTERS_TEST_LIBRARY_DIR, 0); #endif testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }