/********************************************************************* * * Software License Agreement (BSD License) * * move_base2 — test bộ khung chạy end-to-end với thành phần giả. * * Đây là test chứng minh contract giữa các cổng đã khớp: nhận yêu cầu, đi qua đủ state, gọi đúng * cổng, phát đúng lệnh vận tốc, và báo kết quả đúng một lần. * * Author: DuongTD *********************************************************************/ #include #include #include #include #include #include "fake_ports.h" using move_base2::ControlLoop; using move_base2::ControlLoopConfig; using move_base2::ControlLoopDeps; using move_base2::MotionProfile; using move_base2::NavigationRequest; using move_base2::NavigationState; using move_base2::RecoveryTrigger; using move_base2::testing::ActionScript; using move_base2::testing::ControllerScript; using move_base2::testing::FakeActionPort; using move_base2::testing::FakeClockPort; using move_base2::testing::FakeControllerPort; using move_base2::testing::FakeCostmapStatusPort; using move_base2::testing::FakeMissionPort; using move_base2::testing::FakePlannerPort; using move_base2::testing::FakePosePort; using move_base2::testing::FakeRecoveryPort; using move_base2::testing::PlannerScript; using move_base2::testing::RecoveryScript; namespace { constexpr double kControlPeriod = 0.05; ///< [s] ControlLoopConfig baseConfig() { ControlLoopConfig config; config.state_machine.planner_patience = 0.5; // [s] config.state_machine.controller_patience = 0.5; // [s] config.state_machine.oscillation_timeout = 0.0; // tắt config.state_machine.oscillation_distance = 0.5; // [m] config.state_machine.max_planning_retries = -1; config.state_machine.recovery_behavior_count = 2; config.state_machine.recovery_enabled = true; config.velocity.max_vel_x = 0.5; // [m/s] config.velocity.min_vel_x = -0.2; // [m/s] config.velocity.max_vel_theta = 1.0; // [rad/s] config.velocity.max_accel_x = 100.0; // [m/s^2] lớn để test tập trung vào state, không vào ramp config.velocity.max_accel_theta = 100.0; // [rad/s^2] config.nominal_control_period = kControlPeriod; config.position.global_planner_name = "FakeGlobalPlanner"; config.position.local_planner_name = "FakeLocalPlanner"; config.docking = config.position; config.docking.local_planner_name = "FakeDockPlanner"; config.go_straight = config.position; config.go_straight.local_planner_name = "FakeStraightPlanner"; config.rotate = config.position; config.rotate.local_planner_name = "FakeRotatePlanner"; return config; } NavigationRequest makeRequest(double goal_x, std::uint64_t sequence_id = 0) { NavigationRequest request; request.profile = MotionProfile::kPosition; request.goal.header.frame_id = "map"; request.goal.pose.position.x = goal_x; request.goal.pose.orientation.w = 1.0; request.mission_sequence_id = sequence_id; return request; } /// @brief Gắn @p count action tên "act_" vào yêu cầu — kiểm được thứ tự tới ActionPort (D8). NavigationRequest withActions(NavigationRequest request, std::size_t count) { for (std::size_t i = 0; i < count; ++i) { robot_protocol_msgs::Action action; action.actionType = "act_" + std::to_string(i); request.actions.push_back(action); } return request; } /// @brief Yêu cầu chỉ-có-action (D8): has_goal = false, goal để mặc định — không được validate. NavigationRequest makeActionOnlyRequest(std::size_t count, std::uint64_t sequence_id = 0) { NavigationRequest request; request.has_goal = false; request.mission_sequence_id = sequence_id; return withActions(request, count); } /** * @class Fixture * @brief Dựng sẵn control loop nối với toàn bộ cổng giả. */ class Fixture { public: explicit Fixture(const ControlLoopConfig& config = baseConfig()) : recovery_(2) { pose_.setPosition(0.0, 0.0); deps_.clock = &clock_; deps_.pose = &pose_; deps_.planner = &planner_; deps_.controller = &controller_; deps_.recovery = &recovery_; deps_.mission = &mission_; deps_.action = &action_; deps_.costmap_status = &costmap_status_; std::string error; EXPECT_TRUE(loop_.configure(config, deps_, error)) << error; } /// @brief Chạy tối đa @p max_cycles cycle, ghi lại chuỗi state (đã nén lặp liên tiếp). void run(std::size_t max_cycles = 200) { for (std::size_t i = 0; i < max_cycles; ++i) { record(); if (!loop_.step()) { record(); return; } clock_.advance(kControlPeriod); } hit_limit_ = true; } /// @brief Chạy đúng một cycle và ghi lại state sau cycle đó. void stepOnce() { loop_.step(); clock_.advance(kControlPeriod); record(); } const std::vector& states() const { return states_; } bool hitLimit() const { return hit_limit_; } ControlLoop loop_; FakeClockPort clock_; FakePosePort pose_; FakePlannerPort planner_; FakeControllerPort controller_; FakeRecoveryPort recovery_; FakeMissionPort mission_; FakeActionPort action_; FakeCostmapStatusPort costmap_status_; ControlLoopDeps deps_; private: void record() { const std::string name = move_base2::toString(loop_.state()); if (states_.empty() || states_.back() != name) { states_.push_back(name); } } std::vector states_; bool hit_limit_ = false; }; std::string join(const std::vector& items) { std::string out = "["; for (std::size_t i = 0; i < items.size(); ++i) { out += items[i]; if (i + 1 < items.size()) { out += ", "; } } return out + "]"; } } // namespace // ================================================================================================ // Cấu hình và cửa vào // ================================================================================================ TEST(ControlLoop, RefusesToConfigureWithMissingPorts) { ControlLoop loop; ControlLoopDeps deps; // toàn null std::string error; EXPECT_FALSE(loop.configure(baseConfig(), deps, error)); EXPECT_NE(error.find("port"), std::string::npos); EXPECT_FALSE(loop.initialized()); } TEST(ControlLoop, RefusesToConfigureWithInvalidVelocityLimits) { Fixture fixture; ControlLoopConfig bad = baseConfig(); bad.velocity.max_vel_x = -1.0; ControlLoop loop; std::string error; EXPECT_FALSE(loop.configure(bad, fixture.deps_, error)); } TEST(ControlLoop, RefusesToConfigureWithoutPositionPlanner) { Fixture fixture; ControlLoopConfig bad = baseConfig(); bad.position.local_planner_name.clear(); ControlLoop loop; std::string error; EXPECT_FALSE(loop.configure(bad, fixture.deps_, error)); EXPECT_NE(error.find("position"), std::string::npos); } TEST(ControlLoop, RejectsGoalWithInvalidQuaternion) { Fixture fixture; NavigationRequest request = makeRequest(3.0); request.goal.pose.orientation.w = 0.0; // norm = 0 std::string reason; EXPECT_FALSE(fixture.loop_.submit(request, reason)); EXPECT_NE(reason.find("quaternion"), std::string::npos); } TEST(ControlLoop, RejectsGoalWithNonFiniteCoordinates) { Fixture fixture; NavigationRequest request = makeRequest(3.0); request.goal.pose.position.x = std::numeric_limits::quiet_NaN(); std::string reason; EXPECT_FALSE(fixture.loop_.submit(request, reason)); } TEST(ControlLoop, RejectsRequestWhenLocalPlannerCannotBeLoaded) { Fixture fixture; fixture.controller_.setSwapSucceeds(false); std::string reason; EXPECT_FALSE(fixture.loop_.submit(makeRequest(3.0), reason)); EXPECT_NE(reason.find("local planner"), std::string::npos); } TEST(ControlLoop, RejectsRequestWhenGlobalPlannerCannotBeLoaded) { Fixture fixture; fixture.planner_.setSwapSucceeds(false); std::string reason; EXPECT_FALSE(fixture.loop_.submit(makeRequest(3.0), reason)); EXPECT_NE(reason.find("global planner"), std::string::npos); } TEST(ControlLoop, ProfileSelectsItsOwnLocalPlanner) { Fixture fixture; NavigationRequest docking = makeRequest(1.0); docking.profile = MotionProfile::kDocking; docking.marker = "dock_a"; std::string reason; ASSERT_TRUE(fixture.loop_.submit(docking, reason)) << reason; EXPECT_EQ(fixture.controller_.activeController(), "FakeDockPlanner"); // Marker phải tới port TRƯỚC khi swap — planner đọc maker_name trong initialize(). EXPECT_EQ(fixture.controller_.lastDockingMarker(), "dock_a"); NavigationRequest rotate = makeRequest(1.0); rotate.profile = MotionProfile::kRotate; ASSERT_TRUE(fixture.loop_.submit(rotate, reason)) << reason; EXPECT_EQ(fixture.controller_.activeController(), "FakeRotatePlanner"); } TEST(ControlLoop, DockingMarkerProfileOverridesBothPlannersAndFallsBackToDefault) { ControlLoopConfig config = baseConfig(); config.docking_marker_profiles["trolley"].global_planner_name = "FakeTrolleyGlobalPlanner"; config.docking_marker_profiles["trolley"].local_planner_name = "FakeTrolleyLocalPlanner"; Fixture fixture(config); NavigationRequest trolley = makeRequest(1.0); trolley.profile = MotionProfile::kDocking; trolley.marker = "trolley"; std::string reason; ASSERT_TRUE(fixture.loop_.submit(trolley, reason)) << reason; EXPECT_EQ(fixture.planner_.activePlanner(), "FakeTrolleyGlobalPlanner"); EXPECT_EQ(fixture.controller_.activeController(), "FakeTrolleyLocalPlanner"); NavigationRequest unconfigured = makeRequest(1.0); unconfigured.profile = MotionProfile::kDocking; unconfigured.marker = "charger"; ASSERT_TRUE(fixture.loop_.submit(unconfigured, reason)) << reason; EXPECT_EQ(fixture.planner_.activePlanner(), "FakeGlobalPlanner"); EXPECT_EQ(fixture.controller_.activeController(), "FakeDockPlanner"); } TEST(ControlLoop, RejectsDockingRequestWithoutMarker) { Fixture fixture; NavigationRequest docking = makeRequest(1.0); docking.profile = MotionProfile::kDocking; // marker cố ý bỏ trống — bản cũ cũng chặn tại cửa dockTo. std::string reason; EXPECT_FALSE(fixture.loop_.submit(docking, reason)); EXPECT_NE(reason.find("marker"), std::string::npos) << reason; } TEST(ControlLoop, AllowsGoalFrameStyleDockingWithoutMarkerWhenConfigured) { ControlLoopConfig config = baseConfig(); config.docking_requires_marker = false; Fixture fixture(config); NavigationRequest docking = makeRequest(1.0); docking.profile = MotionProfile::kDocking; std::string reason; EXPECT_TRUE(fixture.loop_.submit(docking, reason)) << reason; EXPECT_TRUE(fixture.controller_.lastDockingMarker().empty()); } TEST(ControlLoop, RejectsDockingRequestWhenMarkerIsUnknown) { Fixture fixture; fixture.controller_.setDockingMarkerSucceeds(false); // marker không có trong maker_sources NavigationRequest docking = makeRequest(1.0); docking.profile = MotionProfile::kDocking; docking.marker = "tram_la"; std::string reason; EXPECT_FALSE(fixture.loop_.submit(docking, reason)); EXPECT_NE(reason.find("tram_la"), std::string::npos) << reason; } // ================================================================================================ // Đường đi thuận lợi // ================================================================================================ TEST(ControlLoop, HappyPathReachesSucceededAndReportsOnce) { Fixture fixture; fixture.planner_.setScript({PlannerScript::kOk}); fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kOk, ControllerScript::kOk, ControllerScript::kGoalReached}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 42), reason)) << reason; fixture.run(); EXPECT_FALSE(fixture.hitLimit()); EXPECT_EQ(fixture.states(), (std::vector{"IDLE", "PLANNING", "CONTROLLING", "SUCCEEDED"})) << join(fixture.states()); EXPECT_STREQ(fixture.loop_.lastOutcome(), "SUCCEEDED"); EXPECT_EQ(fixture.loop_.outcomeReportCount(), 1u); EXPECT_EQ(fixture.mission_.reportCountFor(42), 1u) << "each leg may report its outcome exactly once"; } TEST(ControlLoop, DirectGoalWithoutMissionIdDoesNotTouchMissionLayer) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kGoalReached}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 0), reason)) << reason; fixture.run(); EXPECT_EQ(fixture.loop_.outcomeReportCount(), 1u); EXPECT_TRUE(fixture.mission_.reports().empty()) << "a direct goal belongs to no mission so nothing is reported to the mission layer"; } TEST(ControlLoop, ControllerCommandIsPublishedWhileControlling) { Fixture fixture; fixture.controller_.setNominalSpeed(0.3); fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.loop_.step(); // IDLE -> PLANNING (lập plan trong cycle này) fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); // PLANNING -> CONTROLLING, chạy controller ngay ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); EXPECT_NEAR(fixture.loop_.lastCommand().linear.x, 0.3, 1e-9); } TEST(ControlLoop, OrderIsForwardedToThePlanner) { Fixture fixture; NavigationRequest request = makeRequest(3.0); request.order = std::make_shared(); std::string reason; ASSERT_TRUE(fixture.loop_.submit(request, reason)) << reason; fixture.loop_.step(); EXPECT_TRUE(fixture.planner_.sawOrder()); } // ================================================================================================ // Bất biến an toàn // ================================================================================================ TEST(ControlLoop, NoVelocityIsEmittedOutsideControllingAndRecovering) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kOk, ControllerScript::kGoalReached}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; for (int i = 0; i < 50; ++i) { const bool running = fixture.loop_.step(); const NavigationState state = fixture.loop_.state(); if (move_base2::mustBeStopped(state)) { ASSERT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0) << "state " << move_base2::toString(state) << " at cycle " << i; ASSERT_DOUBLE_EQ(fixture.loop_.lastCommand().angular.z, 0.0) << "state " << move_base2::toString(state) << " at cycle " << i; } if (!running) { break; } fixture.clock_.advance(kControlPeriod); } } TEST(ControlLoop, NaNFromControllerIsBlockedByArbiter) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kNaN}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); // CONTROLLING, controller trả NaN EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0); EXPECT_GT(fixture.loop_.arbiter().nonFiniteRejections(), 0u); } TEST(ControlLoop, OverspeedCommandIsClampedNotPassedThrough) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kTooFast}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.5); EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().angular.z, 1.0); EXPECT_GT(fixture.loop_.arbiter().velocityClamps(), 0u); } TEST(ControlLoop, EmptyPlanIsTreatedAsFailureNotAsAValidPlan) { // Fake cố ý vi phạm contract (trả true kèm plan rỗng) để kiểm guard của bên gọi: plan rỗng lọt // xuống sẽ thành front()/back() trên vector rỗng ở tầng dưới. Fixture fixture; fixture.planner_.setScript({PlannerScript::kEmpty}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.run(); EXPECT_EQ(fixture.controller_.setPlanCount(), 0u) << "an empty plan must not be pushed down to " "the controller"; EXPECT_STREQ(fixture.loop_.lastOutcome(), "FAILED"); } TEST(ControlLoop, LostPoseStopsTheRobotImmediately) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); ASSERT_GT(fixture.loop_.lastCommand().linear.x, 0.0); fixture.pose_.setAvailable(false); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0) << "losing TF must stop the robot immediately, not keep going on a stale pose"; } // ================================================================================================ // Recovery // ================================================================================================ TEST(ControlLoop, PrimaryPlannerFailureUsesBackupBeforeRecovery) { ControlLoopConfig config = baseConfig(); config.backup_global_planner_name = "FakeBackupGlobalPlanner"; // 0 nghĩa là không retry planner hiện tại. Backup vẫn phải có đúng một lượt riêng trước recovery. config.state_machine.max_planning_retries = 0; Fixture fixture(config); fixture.planner_.setScript({PlannerScript::kFail, PlannerScript::kOk}); fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kGoalReached}); std::string reason; NavigationRequest request = makeRequest(3.0, 71); request.order = std::make_shared(); ASSERT_TRUE(fixture.loop_.submit(request, reason)) << reason; fixture.run(); EXPECT_FALSE(fixture.hitLimit()); EXPECT_EQ(fixture.planner_.activePlanner(), "FakeBackupGlobalPlanner"); EXPECT_EQ(fixture.planner_.makePlanCount(), 2u); EXPECT_EQ(fixture.planner_.orderHistory(), (std::vector{true, false})); EXPECT_EQ(fixture.recovery_.startCount(), 0u); EXPECT_EQ(fixture.states(), (std::vector{"IDLE", "PLANNING", "CONTROLLING", "SUCCEEDED"})) << join(fixture.states()); } TEST(ControlLoop, BackupPlannerFailureEscalatesToRecovery) { ControlLoopConfig config = baseConfig(); config.backup_global_planner_name = "FakeBackupGlobalPlanner"; config.state_machine.max_planning_retries = 0; Fixture fixture(config); fixture.planner_.setScript({PlannerScript::kFail, PlannerScript::kFail}); fixture.recovery_.setScript({RecoveryScript::kRunning}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 72), reason)) << reason; for (int i = 0; i < 20 && fixture.loop_.state() != NavigationState::kRecovering; ++i) { fixture.stepOnce(); } EXPECT_EQ(fixture.planner_.activePlanner(), "FakeBackupGlobalPlanner"); EXPECT_EQ(fixture.planner_.makePlanCount(), 2u); EXPECT_EQ(fixture.loop_.state(), NavigationState::kRecovering); EXPECT_EQ(fixture.recovery_.startCount(), 1u); EXPECT_EQ(fixture.recovery_.lastTrigger(), RecoveryTrigger::kPlanningFailed); } TEST(ControlLoop, PlannerFailureDrivesRecoveryThenSucceeds) { // Kịch bản thật: planner bế tắc cho tới khi recovery gỡ được thế, sau đó lập plan bình thường. // Lật kịch bản planner ngay khi quan sát thấy RECOVERING, thay vì đếm trước số cycle — đếm trước // làm test phụ thuộc vào đúng thời điểm hết kiên nhẫn, thứ không phải là thứ đang được kiểm. Fixture fixture; fixture.planner_.setScript({PlannerScript::kFail}); fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kGoalReached}); fixture.recovery_.setScript({RecoveryScript::kRunning, RecoveryScript::kSucceeded}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 7), reason)) << reason; bool planner_unblocked = false; std::vector states; for (int i = 0; i < 200; ++i) { const std::string name = move_base2::toString(fixture.loop_.state()); if (states.empty() || states.back() != name) { states.push_back(name); } if (!planner_unblocked && fixture.loop_.state() == NavigationState::kRecovering) { fixture.planner_.setScript({PlannerScript::kOk}); planner_unblocked = true; } if (!fixture.loop_.step()) { states.push_back(move_base2::toString(fixture.loop_.state())); break; } fixture.clock_.advance(kControlPeriod); } EXPECT_TRUE(planner_unblocked) << "never entered recovery"; EXPECT_EQ(states, (std::vector{"IDLE", "PLANNING", "RECOVERING", "PLANNING", "CONTROLLING", "SUCCEEDED"})) << join(states); EXPECT_EQ(fixture.recovery_.startCount(), 1u); EXPECT_EQ(fixture.recovery_.lastStartIndex(), 0u); EXPECT_EQ(fixture.recovery_.lastTrigger(), RecoveryTrigger::kPlanningFailed); EXPECT_EQ(fixture.mission_.reportCountFor(7), 1u); } TEST(ControlLoop, AllRecoveriesExhaustedEndsInAbortedWithSingleReport) { Fixture fixture; fixture.planner_.setScript({PlannerScript::kFail}); // luôn hỏng fixture.recovery_.setScript({RecoveryScript::kSucceeded}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 9), reason)) << reason; fixture.run(); EXPECT_FALSE(fixture.hitLimit()); EXPECT_EQ(fixture.states(), (std::vector{"IDLE", "PLANNING", "RECOVERING", "PLANNING", "RECOVERING", "PLANNING", "ABORTED"})) << join(fixture.states()); EXPECT_EQ(fixture.recovery_.startedIndices(), (std::vector{0u, 1u})) << "behaviors must run one after another, the first one must not repeat"; EXPECT_STREQ(fixture.loop_.lastOutcome(), "FAILED"); EXPECT_EQ(fixture.loop_.outcomeReportCount(), 1u); EXPECT_EQ(fixture.mission_.reportCountFor(9), 1u); } TEST(ControlLoop, RecoveryRefusingToStartMovesOnToTheNextBehavior) { Fixture fixture; fixture.planner_.setScript({PlannerScript::kFail}); fixture.recovery_.setStartSucceeds(false); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.run(); EXPECT_EQ(fixture.recovery_.startedIndices(), (std::vector{0u, 1u})); EXPECT_EQ(fixture.recovery_.updateCount(), 0u) << "a behavior that never started successfully must not be ticked"; EXPECT_STREQ(fixture.loop_.lastOutcome(), "FAILED"); } TEST(ControlLoop, RecoveryVelocityGoesThroughArbiterWithOneHandoverCycle) { Fixture fixture; fixture.planner_.setScript({PlannerScript::kFail}); fixture.recovery_.setScript({RecoveryScript::kRunning, RecoveryScript::kRunning, RecoveryScript::kSucceeded}); fixture.recovery_.setRecoveryVelocity(true, -0.15); // [m/s], âm = lùi fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; bool saw_reverse = false; for (int i = 0; i < 60; ++i) { const bool running = fixture.loop_.step(); if (fixture.loop_.state() == NavigationState::kRecovering && fixture.loop_.lastCommand().linear.x < -1e-6) { saw_reverse = true; EXPECT_GE(fixture.loop_.lastCommand().linear.x, -0.2) << "the reverse command must stay " "within the limit"; } if (!running) { break; } fixture.clock_.advance(kControlPeriod); } EXPECT_TRUE(saw_reverse) << "recovery published a velocity but the command never reached the " "output"; } TEST(ControlLoop, RecoveryDisabledAbortsOnFirstFailure) { ControlLoopConfig config = baseConfig(); config.state_machine.recovery_enabled = false; config.state_machine.recovery_behavior_count = 0; Fixture fixture(config); fixture.planner_.setScript({PlannerScript::kFail}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.run(); EXPECT_EQ(fixture.recovery_.startCount(), 0u); EXPECT_EQ(fixture.states(), (std::vector{"IDLE", "PLANNING", "ABORTED"})) << join(fixture.states()); } // ================================================================================================ // Pause / cancel // ================================================================================================ TEST(ControlLoop, CancelWhileControllingStopsAndReportsCancelled) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 5), reason)) << reason; fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); ASSERT_GT(fixture.loop_.lastCommand().linear.x, 0.0); fixture.loop_.requestCancel(); int cycles_to_zero = 0; for (int i = 0; i < 10; ++i) { fixture.clock_.advance(kControlPeriod); const bool running = fixture.loop_.step(); ++cycles_to_zero; if (!running) { break; } } EXPECT_LE(cycles_to_zero, 2) << "cmd_vel must reach 0 within 2 cycles after a cancel"; EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0); EXPECT_EQ(fixture.loop_.state(), NavigationState::kCancelled); EXPECT_STREQ(fixture.loop_.lastOutcome(), "CANCELLED"); EXPECT_EQ(fixture.mission_.reportCountFor(5), 1u); } TEST(ControlLoop, CancelDuringRecoveryStopsTheBehavior) { Fixture fixture; fixture.planner_.setScript({PlannerScript::kFail}); fixture.recovery_.setScript({RecoveryScript::kRunning}); fixture.recovery_.setRecoveryVelocity(true, -0.15); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; for (int i = 0; i < 40 && fixture.loop_.state() != NavigationState::kRecovering; ++i) { fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); } ASSERT_EQ(fixture.loop_.state(), NavigationState::kRecovering); fixture.loop_.requestCancel(); fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); EXPECT_EQ(fixture.recovery_.cancelCount(), 1u); EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0); fixture.loop_.step(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kCancelled); } TEST(ControlLoop, PauseAndResumeMidControlling) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); fixture.loop_.requestPause(); fixture.clock_.advance(kControlPeriod); fixture.loop_.step(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kPaused); EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0); // Dừng lâu rồi tiếp tục: không được vì thế mà rơi vào recovery. fixture.clock_.advance(30.0); fixture.loop_.requestResume(); fixture.loop_.step(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kControlling); EXPECT_GT(fixture.loop_.lastCommand().linear.x, 0.0); } // ================================================================================================ // Nhiều chặng liên tiếp // ================================================================================================ TEST(ControlLoop, ThreeSequentialMissionLegsEachReportedExactlyOnce) { Fixture fixture; for (std::uint64_t leg = 1; leg <= 3; ++leg) { fixture.planner_.setScript({PlannerScript::kOk}); fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kGoalReached}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(static_cast(leg), leg), reason)) << reason; for (int i = 0; i < 50; ++i) { const bool running = fixture.loop_.step(); fixture.clock_.advance(kControlPeriod); if (!running) { break; } } ASSERT_EQ(fixture.loop_.state(), NavigationState::kSucceeded) << "leg " << leg; ASSERT_EQ(fixture.mission_.reportCountFor(leg), 1u) << "leg " << leg; } EXPECT_EQ(fixture.loop_.outcomeReportCount(), 3u); EXPECT_EQ(fixture.mission_.reports().size(), 3u); } // ================================================================================================ // Action của mission (D8) // ================================================================================================ TEST(ControlLoopActions, ActionOnlyMissionNeverTouchesPlannerOrController) { Fixture fixture; fixture.action_.setScript({ActionScript::kRunning, ActionScript::kSucceeded, ActionScript::kRunning, ActionScript::kSucceeded}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeActionOnlyRequest(2, 7), reason)) << reason; fixture.run(); ASSERT_FALSE(fixture.hitLimit()) << join(fixture.states()); EXPECT_EQ(fixture.states(), (std::vector{"IDLE", "EXECUTING_ACTIONS", "SUCCEEDED"})) << join(fixture.states()); // Không một lời gọi nào tới planner/controller — và vì thế không một lệnh vận tốc nào khác 0. EXPECT_EQ(fixture.planner_.makePlanCount(), 0u); EXPECT_EQ(fixture.controller_.computeCount(), 0u); EXPECT_EQ(fixture.loop_.lastCommand().linear.x, 0.0); EXPECT_EQ(fixture.loop_.lastCommand().angular.z, 0.0); // Hai action tới port nguyên vẹn, đúng thứ tự; kết quả báo đúng một lần sau action cuối. EXPECT_EQ(fixture.action_.startedActionTypes(), (std::vector{"act_0", "act_1"})); EXPECT_EQ(fixture.mission_.reportCountFor(7), 1u); } TEST(ControlLoopActions, MissionWithGoalRunsActionsAfterGoalReached) { Fixture fixture; fixture.planner_.setScript({PlannerScript::kOk}); fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kGoalReached}); fixture.action_.setScript({ActionScript::kRunning, ActionScript::kSucceeded}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(withActions(makeRequest(2.0, 11), 1), reason)) << reason; fixture.run(); ASSERT_FALSE(fixture.hitLimit()) << join(fixture.states()); EXPECT_EQ(fixture.states(), (std::vector{"IDLE", "PLANNING", "CONTROLLING", "EXECUTING_ACTIONS", "SUCCEEDED"})) << join(fixture.states()); EXPECT_EQ(fixture.action_.startedActionTypes(), (std::vector{"act_0"})); EXPECT_EQ(fixture.mission_.reportCountFor(11), 1u); } TEST(ControlLoopActions, ActionFailureAbortsAndReportsOnce) { Fixture fixture; fixture.action_.setScript({ActionScript::kRunning, ActionScript::kFailed}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeActionOnlyRequest(1, 13), reason)) << reason; fixture.run(); ASSERT_FALSE(fixture.hitLimit()) << join(fixture.states()); EXPECT_EQ(fixture.loop_.state(), NavigationState::kAborted); EXPECT_EQ(fixture.mission_.reportCountFor(13), 1u); EXPECT_EQ(fixture.recovery_.startCount(), 0u) << "a failed action must not drag recovery in"; } TEST(ControlLoopActions, SubmitRejectsActionRequestWhenActionPortMissing) { Fixture fixture; ControlLoop bare_loop; ControlLoopDeps deps = fixture.deps_; deps.action = nullptr; std::string error; ASSERT_TRUE(bare_loop.configure(baseConfig(), deps, error)) << error; std::string reason; EXPECT_FALSE(bare_loop.submit(makeActionOnlyRequest(1), reason)); EXPECT_NE(reason.find("action port"), std::string::npos) << reason; // Không goal lẫn action thì bị từ chối bất kể có port hay không. EXPECT_FALSE(fixture.loop_.submit(makeActionOnlyRequest(0), reason)); EXPECT_NE(reason.find("neither goal nor action"), std::string::npos) << reason; } // ================================================================================================ // Lập plan bất đồng bộ (kBusy) // // Global planner nặng mất hàng trăm ms, control loop chạy 20 Hz và là thread duy nhất phát cmd_vel. // Các test dưới đây khoá lại điều kiện để tách được hai nhịp đó mà state machine không phải đoán. // ================================================================================================ TEST(ControlLoopAsyncPlanner, StaysInPlanningWhileThePlannerIsStillWorking) { Fixture fixture; fixture.planner_.setLatencyCycles(3); fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.stepOnce(); // IDLE -> PLANNING, kick lượt lập plan ASSERT_EQ(fixture.loop_.state(), NavigationState::kPlanning); for (int i = 0; i < 3; ++i) { fixture.stepOnce(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kPlanning) << "left PLANNING while the planner was still computing, cycle " << i; } fixture.stepOnce(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kControlling); } TEST(ControlLoopAsyncPlanner, DoesNotRestartAPlanThatIsAlreadyRunning) { // `start_planner` là tín hiệu MỨC, bật lại mỗi cycle chừng nào còn ở PLANNING. Kick lại một lượt // đang chạy sẽ làm mất toàn bộ thời gian đã bỏ ra cho nó. Fixture fixture; fixture.planner_.setLatencyCycles(4); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; for (int i = 0; i < 4; ++i) { fixture.stepOnce(); } EXPECT_EQ(fixture.planner_.makePlanCount(), 1u) << "the planning attempt was restarted every cycle"; } TEST(ControlLoopAsyncPlanner, PlannerPatienceStillFiresWhileThePlannerIsBusy) { // Đây là thứ DUY NHẤT phát hiện được thread planner treo. Ở chế độ đồng bộ, planner treo làm // treo luôn control loop nên còn dễ thấy; ở chế độ bất đồng bộ nó im lặng hoàn toàn. ControlLoopConfig config = baseConfig(); config.state_machine.planner_patience = 0.2; // [s] = 4 cycle config.state_machine.max_planning_retries = -1; Fixture fixture(config); fixture.planner_.setLatencyCycles(1000); // không bao giờ xong std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; for (int i = 0; i < 20; ++i) { fixture.stepOnce(); } // Planner treo vĩnh viễn thì đường đi đúng là: hết kiên nhẫn -> recovery -> thử lại -> hết // recovery -> ABORTED. Điều phải khoá lại là nó KHÔNG đứng im ở PLANNING. EXPECT_NE(std::find(fixture.states().begin(), fixture.states().end(), "RECOVERING"), fixture.states().end()) << "the planner hung and nobody escalated — the robot would sit in PLANNING forever: " << join(fixture.states()); EXPECT_EQ(fixture.loop_.state(), NavigationState::kAborted) << join(fixture.states()); } TEST(ControlLoopAsyncPlanner, KeepsFollowingTheOldPlanWhileReplanningInBackground) { // Chính là lý do tồn tại của cả mô hình: lập lại plan không được làm gián đoạn việc bám plan cũ. ControlLoopConfig config = baseConfig(); config.state_machine.controller_patience = 100.0; // [s] không cho controller hết kiên nhẫn Fixture fixture(config); fixture.controller_.setNominalSpeed(0.3); // [m/s] fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.stepOnce(); // PLANNING fixture.stepOnce(); // -> CONTROLLING ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); for (int i = 0; i < 5; ++i) { fixture.stepOnce(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kControlling); EXPECT_NEAR(fixture.loop_.lastCommand().linear.x, 0.3, 1e-9) << "cmd_vel was interrupted while replanning, cycle " << i; } } TEST(ControlLoopAsyncPlanner, CancelWhilePlanningCancelsTheRunningPlan) { // Không huỷ thì lượt đang bay vẫn về và chiếm chỗ hộp thư, rồi bị nhận nhầm cho yêu cầu kế tiếp. Fixture fixture; fixture.planner_.setLatencyCycles(100); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.stepOnce(); ASSERT_EQ(fixture.loop_.state(), NavigationState::kPlanning); ASSERT_TRUE(fixture.planner_.isPlanning()); fixture.loop_.requestCancel(); fixture.stepOnce(); EXPECT_GE(fixture.planner_.cancelCount(), 1u); EXPECT_FALSE(fixture.planner_.isPlanning()); } TEST(ControlLoopAsyncPlanner, AcceptingANewRequestCancelsAnInFlightPlan) { Fixture fixture; fixture.planner_.setLatencyCycles(100); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.stepOnce(); ASSERT_TRUE(fixture.planner_.isPlanning()); const std::size_t before = fixture.planner_.cancelCount(); fixture.loop_.requestCancel(); fixture.stepOnce(); fixture.stepOnce(); ASSERT_TRUE(fixture.loop_.submit(makeRequest(5.0), reason)) << reason; fixture.stepOnce(); EXPECT_GT(fixture.planner_.cancelCount(), before) << "a new request did not cancel the planning attempt of the old goal"; } TEST(ControlLoopAsyncPlanner, FailureToStartAPlanIsTreatedAsAFailedAttempt) { // Không khởi động được mà im lặng thì state machine đứng ở PLANNING vô hạn: không có kFailed để // đếm lượt, không có kPlanReady để đi tiếp. ControlLoopConfig config = baseConfig(); config.state_machine.max_planning_retries = 2; Fixture fixture(config); fixture.planner_.setSwapSucceeds(true); fixture.planner_.setScript({PlannerScript::kFail}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.run(); EXPECT_FALSE(fixture.hitLimit()) << join(fixture.states()); EXPECT_NE(std::find(fixture.states().begin(), fixture.states().end(), "RECOVERING"), fixture.states().end()) << join(fixture.states()); } // ================================================================================================ // Preempt — goal mới thay goal cũ NGAY // // Bấm goal mới nghĩa là goal cũ không còn muốn nữa. Xếp hàng chờ robot đi hết chặng cũ là hành vi // không ai mong đợi, và mission layer cũng đã chốt "preempt ngay" (Q1, Phase 2). // ================================================================================================ TEST(ControlLoopPreempt, NewGoalWhileControllingReplansImmediately) { Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.stepOnce(); // IDLE -> PLANNING fixture.stepOnce(); // -> CONTROLLING ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); const std::size_t plans_before = fixture.planner_.makePlanCount(); ASSERT_TRUE(fixture.loop_.submit(makeRequest(9.0), reason)) << reason; fixture.stepOnce(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kPlanning) << "the new goal was queued instead of replacing the old one right away"; EXPECT_GT(fixture.planner_.makePlanCount(), plans_before) << "did not replan for the new goal"; } TEST(ControlLoopPreempt, NewGoalWhilePlanningReplansImmediately) { Fixture fixture; fixture.planner_.setLatencyCycles(50); // plan cũ còn lâu mới xong std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; fixture.stepOnce(); ASSERT_EQ(fixture.loop_.state(), NavigationState::kPlanning); ASSERT_TRUE(fixture.loop_.submit(makeRequest(9.0), reason)) << reason; fixture.stepOnce(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kPlanning); EXPECT_GE(fixture.planner_.cancelCount(), 1u) << "the planning attempt for the old goal was not " "cancelled"; } TEST(ControlLoopPreempt, OldMissionIsReportedPreemptedExactlyOnceUnderItsOwnId) { // Bất biến quan trọng nhất: báo kết quả ĐÚNG MỘT LẦN và ĐÚNG ID. Preempt báo kết quả chặng cũ và // nhận chặng mới trong cùng một cycle — dùng nhầm id thì mission layer mất dấu cả hai chặng. Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 11), reason)) << reason; fixture.stepOnce(); fixture.stepOnce(); ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); ASSERT_TRUE(fixture.loop_.submit(makeRequest(9.0, 22), reason)) << reason; fixture.stepOnce(); EXPECT_EQ(fixture.mission_.reportCountFor(11), 1u) << "the preempted leg was not reported " "exactly once"; EXPECT_EQ(fixture.mission_.reportCountFor(22), 0u) << "the NEW leg got its outcome reported the " "moment it was accepted"; } TEST(ControlLoopPreempt, PreemptedGoalStillFinishesTheNewOne) { // Preempt không được để lại trạng thái nửa vời: chặng mới phải chạy tới cùng như bình thường. Fixture fixture; fixture.controller_.setScript({ControllerScript::kOk, ControllerScript::kOk, ControllerScript::kGoalReached}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0, 11), reason)) << reason; fixture.stepOnce(); fixture.stepOnce(); ASSERT_TRUE(fixture.loop_.submit(makeRequest(9.0, 22), reason)) << reason; fixture.run(); EXPECT_FALSE(fixture.hitLimit()) << join(fixture.states()); EXPECT_STREQ(fixture.loop_.lastOutcome(), "SUCCEEDED"); EXPECT_EQ(fixture.mission_.reportCountFor(22), 1u); } TEST(ControlLoopPreempt, NewGoalDuringRecoveryCancelsTheRunningBehavior) { ControlLoopConfig config = baseConfig(); config.state_machine.planner_patience = 0.05; // [s] vào recovery nhanh Fixture fixture(config); fixture.planner_.setScript({PlannerScript::kFail}); fixture.recovery_.setScript({RecoveryScript::kRunning}); std::string reason; ASSERT_TRUE(fixture.loop_.submit(makeRequest(3.0), reason)) << reason; for (int i = 0; i < 6 && fixture.loop_.state() != NavigationState::kRecovering; ++i) { fixture.stepOnce(); } ASSERT_EQ(fixture.loop_.state(), NavigationState::kRecovering) << join(fixture.states()); fixture.planner_.setScript({PlannerScript::kOk}); ASSERT_TRUE(fixture.loop_.submit(makeRequest(9.0), reason)) << reason; fixture.stepOnce(); EXPECT_EQ(fixture.loop_.state(), NavigationState::kPlanning); EXPECT_GE(fixture.recovery_.cancelCount(), 1u) << "the running behavior was not told to stop"; } // ================================================================================================ // Guard "không đi mù" — dữ liệu quan sát của costmap quá hạn // ================================================================================================ // // move_base thế hệ 1 có đúng guard này (`move_base.cpp:2720`) và move_base2 trước đây KHÔNG có: // costmap hết hạn nghĩa là robot đang tránh vật cản trên một bản đồ của quá khứ. TEST(StaleCostmap, BlocksWheelsWhileControlling) { Fixture fixture; fixture.planner_.setScript({ PlannerScript::kOk }); fixture.controller_.setScript(std::vector(50, ControllerScript::kOk)); std::string error; ASSERT_TRUE(fixture.loop_.submit(makeRequest(2.0), error)) << error; // Chạy tới khi đang bám plan và thực sự có lệnh khác 0. for (int i = 0; i < 20 && fixture.loop_.state() != NavigationState::kControlling; ++i) { fixture.loop_.step(); } ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); fixture.loop_.step(); ASSERT_GT(std::abs(fixture.loop_.lastCommand().linear.x), 0.0) << "no command to block yet"; const std::size_t controller_calls_before = fixture.controller_.computeCount(); fixture.costmap_status_.setCurrent(false); fixture.loop_.step(); EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0) << "sensor data is stale yet it kept " "driving"; EXPECT_DOUBLE_EQ(fixture.loop_.lastCommand().angular.z, 0.0); EXPECT_EQ(fixture.controller_.computeCount(), controller_calls_before) << "the controller must not compute a command on stale data"; EXPECT_EQ(fixture.loop_.state(), NavigationState::kControlling) << "the guard only blocks the wheels, it does not change state — exactly like the old " "version"; } TEST(StaleCostmap, ResumesWhenSensorDataBecomesCurrentAgain) { Fixture fixture; fixture.planner_.setScript({ PlannerScript::kOk }); fixture.controller_.setScript(std::vector(50, ControllerScript::kOk)); std::string error; ASSERT_TRUE(fixture.loop_.submit(makeRequest(2.0), error)) << error; for (int i = 0; i < 20 && fixture.loop_.state() != NavigationState::kControlling; ++i) { fixture.loop_.step(); } ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); fixture.costmap_status_.setCurrent(false); fixture.loop_.step(); ASSERT_DOUBLE_EQ(fixture.loop_.lastCommand().linear.x, 0.0); fixture.costmap_status_.setCurrent(true); fixture.loop_.step(); EXPECT_GT(std::abs(fixture.loop_.lastCommand().linear.x), 0.0) << "sensors are fresh again yet the robot stays still"; } TEST(StaleCostmap, DisabledByConfigLetsTheRobotDrive) { ControlLoopConfig config = baseConfig(); config.require_current_costmap = false; Fixture fixture(config); fixture.planner_.setScript({ PlannerScript::kOk }); fixture.controller_.setScript(std::vector(50, ControllerScript::kOk)); std::string error; ASSERT_TRUE(fixture.loop_.submit(makeRequest(2.0), error)) << error; for (int i = 0; i < 20 && fixture.loop_.state() != NavigationState::kControlling; ++i) { fixture.loop_.step(); } ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); fixture.costmap_status_.setCurrent(false); fixture.loop_.step(); EXPECT_GT(std::abs(fixture.loop_.lastCommand().linear.x), 0.0) << "the guard is disabled by config yet it still blocked"; } TEST(StaleCostmap, NullPortMeansNoGuard) { // Đường đi của mọi test cổng-giả có sẵn: không ai bơm costmap_status thì lõi coi là còn hạn. Fixture fixture; fixture.deps_.costmap_status = nullptr; std::string error; ASSERT_TRUE(fixture.loop_.configure(baseConfig(), fixture.deps_, error)) << error; fixture.planner_.setScript({ PlannerScript::kOk }); fixture.controller_.setScript(std::vector(50, ControllerScript::kOk)); ASSERT_TRUE(fixture.loop_.submit(makeRequest(2.0), error)) << error; for (int i = 0; i < 20 && fixture.loop_.state() != NavigationState::kControlling; ++i) { fixture.loop_.step(); } ASSERT_EQ(fixture.loop_.state(), NavigationState::kControlling); fixture.costmap_status_.setCurrent(false); // không ai hỏi nó cả fixture.loop_.step(); EXPECT_GT(std::abs(fixture.loop_.lastCommand().linear.x), 0.0); EXPECT_EQ(fixture.costmap_status_.queryCount(), 0u) << "the port was detached yet the core still " "asked it"; } int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }