/********************************************************************* * * Software License Agreement (BSD License) * * move_base2 — test PlannerRunner: nạp plugin thật qua Boost.DLL, và mọi đường lỗi phải trả false * chứ không được để dữ liệu hỏng đi tiếp. * * Author: DuongTD *********************************************************************/ #include #include #include #include #include #include #include #include #include #include namespace { using move_base2::PlannerRunner; /** * @brief Con trỏ costmap giả. * * `PlannerRunner::configure` từ chối costmap null — đúng, vì mọi plugin thật đều dùng nó. Nhưng * `Costmap2DROBOT` không dựng được trong unit test (cần `tf3::BufferCore` thật và cây config đầy * đủ), nên test dùng một địa chỉ hợp lệ nhưng không phải costmap. * * An toàn ở đây vì `test_global_planner.cpp` **không alias nào chạm vào con trỏ này** — nó chỉ được * chuyển tiếp qua `initialize()` rồi bị bỏ qua. Đường có costmap thật thuộc test tích hợp (Phase 5). */ robot_costmap_2d::Costmap2DROBOT* dummyCostmap() { static std::uintptr_t placeholder = 0; return reinterpret_cast(&placeholder); } /// @brief Chạy trọn một lượt lập plan đồng bộ hoá lại cho test: kick, chờ thread, lấy kết quả. bool runOnePlan(move_base2::PlannerRunner& runner, const robot_geometry_msgs::PoseStamped& start, const robot_geometry_msgs::PoseStamped& goal, const robot_protocol_msgs::Order* order, move_base2::PlanResult& result) { if (!runner.startPlan(start, goal, order, /*tag=*/1)) { return false; } // Thread planner là thread thật; test phải chờ nó. Vòng quay ngắn thay vì sleep cố định để test // không phụ thuộc vào tốc độ máy. for (int i = 0; i < 10000 && runner.isPlanning(); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return runner.pollPlan(result); } robot_geometry_msgs::PoseStamped makePose(double x, double y) { robot_geometry_msgs::PoseStamped pose; pose.header.frame_id = "map"; pose.pose.position.x = x; // [m] pose.pose.position.y = y; // [m] pose.pose.orientation.w = 1.0; return pose; } /// @brief Runner đã configure với planner @p name; ASSERT nếu không nạp được. class Fixture { public: explicit Fixture(const std::string& name = "TestPlannerOk") { robot::NodeHandle nh; std::string error; ok_ = runner_.configure(nh, dummyCostmap(), name, error); error_ = error; } bool ok() const { return ok_; } const std::string& error() const { return error_; } PlannerRunner runner_; private: bool ok_ = false; std::string error_; }; } // namespace // ================================================================================================ // Cấu hình // ================================================================================================ TEST(PlannerRunner, RefusesNullCostmap) { // Plugin thật nào cũng dùng costmap. Nhận null rồi chuyển tiếp xuống `initialize()` là đẩy quyết // định "sập hay không" cho từng plugin tự lo. robot::NodeHandle nh; PlannerRunner runner; std::string error; EXPECT_FALSE(runner.configure(nh, nullptr, "TestPlannerOk", error)); EXPECT_FALSE(error.empty()); EXPECT_FALSE(runner.configured()); } TEST(PlannerRunner, ConfiguresWithoutAnInitialPlanner) { robot::NodeHandle nh; PlannerRunner runner; std::string error; ASSERT_TRUE(runner.configure(nh, dummyCostmap(), "", error)) << error; EXPECT_TRUE(runner.configured()); EXPECT_TRUE(runner.activePlanner().empty()); EXPECT_EQ(runner.loadedCount(), 0U); } TEST(PlannerRunner, RefusesSecondConfigure) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); robot::NodeHandle nh; std::string error; EXPECT_FALSE(fixture.runner_.configure(nh, dummyCostmap(), "TestPlannerOk", error)); } TEST(PlannerRunner, ConfigureFailsWhenTheInitialPlannerCannotBeLoaded) { robot::NodeHandle nh; PlannerRunner runner; std::string error; EXPECT_FALSE(runner.configure(nh, dummyCostmap(), "TestPlannerMissingLibrary", error)); EXPECT_FALSE(error.empty()); EXPECT_FALSE(runner.configured()) << "configure failed but the object still reports itself as " "configured"; } // ================================================================================================ // Nạp plugin qua Boost.DLL // ================================================================================================ TEST(PlannerRunner, LoadsTheInitialPlannerAndReportsItAsActive) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); EXPECT_EQ(fixture.runner_.activePlanner(), "TestPlannerOk"); EXPECT_EQ(fixture.runner_.loadedCount(), 1U); } TEST(PlannerRunner, SwapsBetweenPlannersAndReusesLoadedLibraries) { // swapPlanner chạy ở CỬA VÀO mỗi yêu cầu. Đổi qua lại giữa hai profile không được dlopen lại. Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); ASSERT_TRUE(fixture.runner_.swapPlanner("TestPlannerEmptyPlan")); EXPECT_EQ(fixture.runner_.activePlanner(), "TestPlannerEmptyPlan"); EXPECT_EQ(fixture.runner_.loadedCount(), 2U); ASSERT_TRUE(fixture.runner_.swapPlanner("TestPlannerOk")); EXPECT_EQ(fixture.runner_.activePlanner(), "TestPlannerOk"); EXPECT_EQ(fixture.runner_.loadedCount(), 2U) << "switched back to the previous planner yet " "reloaded the library"; } TEST(PlannerRunner, FailedSwapKeepsThePreviousPlannerActive) { // Bên gọi từ chối yêu cầu dựa trên giá trị trả về. Chuyển sang trạng thái "không có planner" sẽ // giết luôn yêu cầu đang chạy dở, dù nó chẳng liên quan gì tới planner vừa nạp hỏng. Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); EXPECT_FALSE(fixture.runner_.swapPlanner("TestPlannerMissingLibrary")); EXPECT_EQ(fixture.runner_.activePlanner(), "TestPlannerOk"); move_base2::PlanResult result; ASSERT_TRUE(runOnePlan(fixture.runner_, makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, result)); EXPECT_TRUE(result.succeeded); } TEST(PlannerRunner, PlannerReportingInitializeFailureIsRejected) { // Bản cũ chỉ log rồi đi tiếp với planner chưa khởi tạo xong. robot::NodeHandle nh; PlannerRunner runner; std::string error; EXPECT_FALSE(runner.configure(nh, dummyCostmap(), "TestPlannerInitFails", error)); } TEST(PlannerRunner, PlannerThatFailedToInitializeIsNotCached) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); EXPECT_FALSE(fixture.runner_.swapPlanner("TestPlannerInitFails")); EXPECT_EQ(fixture.runner_.loadedCount(), 1U) << "a broken instance was cached — every later attempt would get that same broken one back"; } TEST(PlannerRunner, RefusesEmptyPlannerName) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); EXPECT_FALSE(fixture.runner_.swapPlanner("")); EXPECT_EQ(fixture.runner_.activePlanner(), "TestPlannerOk"); } TEST(PlannerRunner, SwapBeforeConfigureIsRefused) { PlannerRunner runner; EXPECT_FALSE(runner.swapPlanner("TestPlannerOk")); } // ================================================================================================ // Một lượt lập plan — mọi đường lỗi phải báo thất bại, plan phải rỗng // ================================================================================================ TEST(PlannerRunner, ProducesANonEmptyPlanOnTheHappyPath) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); move_base2::PlanResult result; ASSERT_TRUE(runOnePlan(fixture.runner_, makePose(0.0, 0.0), makePose(2.0, 1.0), nullptr, result)); ASSERT_TRUE(result.succeeded); ASSERT_FALSE(result.plan.empty()); EXPECT_EQ(result.tag, 1U); EXPECT_DOUBLE_EQ(result.plan.back().pose.position.x, 2.0); EXPECT_DOUBLE_EQ(result.plan.back().pose.position.y, 1.0); } TEST(PlannerRunner, OrderIsForwardedToTheOrderAwareOverload) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); const robot_protocol_msgs::Order order; move_base2::PlanResult result; ASSERT_TRUE(runOnePlan(fixture.runner_, makePose(0.0, 0.0), makePose(2.0, 0.0), &order, result)); EXPECT_TRUE(result.succeeded); EXPECT_FALSE(result.plan.empty()); } TEST(PlannerRunner, OrderIsCopiedSoItMayDieBeforeThePlanFinishes) { // Con trỏ Order chỉ hợp lệ trong lời gọi startPlan, nhưng lượt lập plan sống lâu hơn thế. Không // sao chép là thread planner đọc bộ nhớ đã chết. Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); { const robot_protocol_msgs::Order order; ASSERT_TRUE(fixture.runner_.startPlan(makePose(0.0, 0.0), makePose(2.0, 0.0), &order, 7)); } // order chết ở đây for (int i = 0; i < 10000 && fixture.runner_.isPlanning(); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } move_base2::PlanResult result; ASSERT_TRUE(fixture.runner_.pollPlan(result)); EXPECT_TRUE(result.succeeded); EXPECT_EQ(result.tag, 7U); } TEST(PlannerRunner, PlannerReturningTrueWithAnEmptyPlanIsTreatedAsFailure) { // Contract của PlannerPort: thành công nghĩa là plan KHÔNG rỗng. Lọt qua thì tầng trên gọi // front()/back() trên vector rỗng. Fixture fixture("TestPlannerEmptyPlan"); ASSERT_TRUE(fixture.ok()) << fixture.error(); move_base2::PlanResult result; ASSERT_TRUE(runOnePlan(fixture.runner_, makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, result)); EXPECT_FALSE(result.succeeded); EXPECT_TRUE(result.plan.empty()); } TEST(PlannerRunner, ExceptionFromThePluginIsContained) { // Plugin là code bên thứ ba nạp lúc chạy. Exception thoát khỏi thân thread là std::terminate — // mất cả tiến trình navigation vì một lượt lập plan hỏng. Fixture fixture("TestPlannerThrowing"); ASSERT_TRUE(fixture.ok()) << fixture.error(); move_base2::PlanResult result; ASSERT_TRUE(runOnePlan(fixture.runner_, makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, result)); EXPECT_FALSE(result.succeeded); EXPECT_TRUE(result.plan.empty()); } TEST(PlannerRunner, NonFiniteStartOrGoalIsRejectedBeforeStartingTheThread) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); const double nan = std::numeric_limits::quiet_NaN(); const double inf = std::numeric_limits::infinity(); EXPECT_FALSE(fixture.runner_.startPlan(makePose(nan, 0.0), makePose(2.0, 0.0), nullptr, 1)); EXPECT_FALSE(fixture.runner_.startPlan(makePose(0.0, 0.0), makePose(inf, 0.0), nullptr, 1)); EXPECT_FALSE(fixture.runner_.isPlanning()); } TEST(PlannerRunner, StartPlanWithoutAnActivePlannerFails) { robot::NodeHandle nh; PlannerRunner runner; std::string error; ASSERT_TRUE(runner.configure(nh, dummyCostmap(), "", error)) << error; EXPECT_FALSE(runner.startPlan(makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, 1)); } TEST(PlannerRunner, SecondStartWhileOneIsInFlightIsRefused) { // Một lượt tại một thời điểm. Nhận thêm sẽ đè lên yêu cầu đang chạy và làm mất công đã bỏ ra. Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); ASSERT_TRUE(fixture.runner_.startPlan(makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, 1)); const bool refused = !fixture.runner_.startPlan(makePose(0.0, 0.0), makePose(3.0, 0.0), nullptr, 2); for (int i = 0; i < 10000 && fixture.runner_.isPlanning(); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } // Lượt đầu có thể đã xong trước lời gọi thứ hai (planner giả rất nhanh), nên chỉ khẳng định điều // luôn đúng: không bao giờ có hai lượt cùng chạy, và kết quả thu về là của MỘT lượt. move_base2::PlanResult result; ASSERT_TRUE(fixture.runner_.pollPlan(result)); EXPECT_TRUE(result.tag == 1U || (!refused && result.tag == 2U)); EXPECT_FALSE(fixture.runner_.pollPlan(result)) << "a second result is still sitting in the " "mailbox"; } TEST(PlannerRunner, CancelledPlanProducesNoResult) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); ASSERT_TRUE(fixture.runner_.startPlan(makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, 1)); fixture.runner_.cancelPlan(); for (int i = 0; i < 10000 && fixture.runner_.isPlanning(); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } move_base2::PlanResult result; EXPECT_FALSE(fixture.runner_.pollPlan(result)) << "a cancelled attempt still returned a result — the caller would follow a plan to a goal " "nobody asks for anymore"; } TEST(PlannerRunner, ResultCarriesBackTheTagItWasStartedWith) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); ASSERT_TRUE(fixture.runner_.startPlan(makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, 42)); for (int i = 0; i < 10000 && fixture.runner_.isPlanning(); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } move_base2::PlanResult result; ASSERT_TRUE(fixture.runner_.pollPlan(result)); EXPECT_EQ(result.tag, 42U); } TEST(PlannerRunner, PollOnAnIdleRunnerReturnsNothing) { Fixture fixture; ASSERT_TRUE(fixture.ok()) << fixture.error(); move_base2::PlanResult result; EXPECT_FALSE(fixture.runner_.pollPlan(result)); EXPECT_FALSE(fixture.runner_.isPlanning()); } TEST(PlannerRunner, DestructorJoinsWhileAPlanIsInFlight) { // Detach thay vì join sẽ để thread chạm vào buffer đã bị huỷ. Test này chạy sạch dưới sanitizer // là bằng chứng; ở đây nó ít nhất khẳng định destructor không treo. robot::NodeHandle nh; std::string error; { PlannerRunner runner; ASSERT_TRUE(runner.configure(nh, dummyCostmap(), "TestPlannerOk", error)) << error; EXPECT_TRUE(runner.startPlan(makePose(0.0, 0.0), makePose(2.0, 0.0), nullptr, 1)); } SUCCEED(); } int main(int argc, char** argv) { // ctest không mang theo biến môi trường của shell; binary tự trỏ vào cây config và thư viện của // gói, đúng cách recovery_runner_test và action_runner_test đang làm. setenv("PNKX_NAV_CORE_CONFIG_DIR", MOVE_BASE2_TEST_CONFIG_DIR, 0); setenv("PNKX_NAV_CORE_LIBRARY_PATH", MOVE_BASE2_TEST_LIBRARY_DIR, 0); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }