/********************************************************************* * * Kiểm bất biến vòng đời của RecoveryBehavior: guard configure/start/update, kiểm ngữ cảnh bắt * buộc theo họ output, dt đo bằng đồng hồ thật, và đường cancel. * * Author: DuongTD *********************************************************************/ #include #include #include #include "recovery_test_utils.h" namespace { using recovery_core::RecoveryGoal; using recovery_core::RecoveryOutputType; using recovery_core::RecoveryStatus; using recovery_test::MockBehavior; using recovery_test::VelocityRig; TEST(RecoveryLifecycle, StartBeforeConfigureFails) { MockBehavior behavior(RecoveryOutputType::kNone); const robot::Time now(1000.0); EXPECT_FALSE(behavior.start(RecoveryGoal(), now)); EXPECT_EQ(behavior.start_calls, 0); } TEST(RecoveryLifecycle, UpdateBeforeStartReturnsFailedStopOutput) { MockBehavior behavior(RecoveryOutputType::kNone); const robot::Time now(1000.0); const auto result = behavior.update(now); EXPECT_EQ(result.status, RecoveryStatus::kFailed); EXPECT_EQ(result.output_type, RecoveryOutputType::kNone); EXPECT_EQ(behavior.update_calls, 0); } TEST(RecoveryLifecycle, ConfigureTwiceRejected) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; EXPECT_TRUE(behavior.configure("mock", ctx, nh)); // Gọi lần hai với ctx khác là lỗi lập trình của caller — phải báo, không được nuốt. EXPECT_FALSE(behavior.configure("mock", ctx, nh)); EXPECT_EQ(behavior.configure_calls, 1); } TEST(RecoveryLifecycle, EmptyInstanceNameRejected) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; EXPECT_FALSE(behavior.configure("", ctx, nh)); EXPECT_EQ(behavior.configure_calls, 0); } TEST(RecoveryLifecycle, VelocityFamilyRequiresPoseProvider) { VelocityRig rig; MockBehavior behavior(RecoveryOutputType::kVelocity); robot::NodeHandle nh; recovery_core::RecoveryContext ctx = rig.ctx; ctx.pose = nullptr; // Không có pose thì tiến độ chỉ có thể dead-reckon — đúng lớp lỗi Phase 3 phải diệt. EXPECT_FALSE(behavior.configure("mock", ctx, nh)); } TEST(RecoveryLifecycle, VelocityFamilyRequiresCollisionChecker) { VelocityRig rig; MockBehavior behavior(RecoveryOutputType::kVelocity); robot::NodeHandle nh; recovery_core::RecoveryContext ctx = rig.ctx; ctx.collision = nullptr; EXPECT_FALSE(behavior.configure("mock", ctx, nh)); } TEST(RecoveryLifecycle, PathFamilyRequiresPlanProvider) { MockBehavior behavior(RecoveryOutputType::kPath); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; EXPECT_FALSE(behavior.configure("mock", ctx, nh)); } TEST(RecoveryLifecycle, NoneFamilyNeedsNoPorts) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; EXPECT_TRUE(behavior.configure("mock", ctx, nh)); } TEST(RecoveryLifecycle, ConfigureFailsWhenPluginRejects) { MockBehavior behavior(RecoveryOutputType::kNone); behavior.configure_ok = false; robot::NodeHandle nh; recovery_core::RecoveryContext ctx; EXPECT_FALSE(behavior.configure("mock", ctx, nh)); // Không được coi là đã cấu hình: start() sau đó phải hỏng. EXPECT_FALSE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); } TEST(RecoveryLifecycle, StartRejectedWhenPluginRefuses) { MockBehavior behavior(RecoveryOutputType::kNone); behavior.start_ok = false; robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); EXPECT_FALSE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); EXPECT_EQ(behavior.status(), RecoveryStatus::kFailed); } TEST(RecoveryLifecycle, TerminalStateDoesNotTickAgain) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); behavior.next_result = recovery_core::RecoveryResult::Succeeded(); ASSERT_EQ(behavior.update(robot::Time(1000.1)).status, RecoveryStatus::kSucceeded); const int calls_after_success = behavior.update_calls; const auto again = behavior.update(robot::Time(1000.2)); EXPECT_EQ(again.status, RecoveryStatus::kSucceeded); EXPECT_EQ(behavior.update_calls, calls_after_success); // không gọi thêm onUpdate } TEST(RecoveryLifecycle, CancelYieldsCancelledStopOutput) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); behavior.cancel(); const auto result = behavior.update(robot::Time(1000.1)); EXPECT_EQ(result.status, RecoveryStatus::kCancelled); EXPECT_EQ(behavior.cancel_calls, 1); EXPECT_EQ(behavior.update_calls, 0); // cancel thay thế tick, không chạy logic plugin } TEST(RecoveryLifecycle, CancelOnVelocityFamilyEmitsExplicitZeroTwist) { VelocityRig rig; MockBehavior behavior(RecoveryOutputType::kVelocity); robot::NodeHandle nh; ASSERT_TRUE(behavior.configure("mock", rig.ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); behavior.cancel(); const auto result = behavior.update(robot::Time(1000.1)); // Họ velocity phải nhận lệnh dừng TƯỜNG MINH: caller đang lấy cmd_vel từ đây. ASSERT_NE(result.velocity(), nullptr); EXPECT_DOUBLE_EQ(result.velocity()->linear.x, 0.0); EXPECT_DOUBLE_EQ(result.velocity()->angular.z, 0.0); } TEST(RecoveryLifecycle, DtMeasuredFromRealClockNotConfiguredPeriod) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); behavior.update(robot::Time(1000.0)); EXPECT_NEAR(behavior.last_dt, 0.0, 1e-9); // tick đầu ngay sau start behavior.update(robot::Time(1000.5)); EXPECT_NEAR(behavior.last_dt, 0.5, 1e-6); behavior.update(robot::Time(1002.5)); EXPECT_NEAR(behavior.last_dt, 2.0, 1e-6); // loop chạy chậm -> dt lớn, không phải hằng số config } TEST(RecoveryLifecycle, BackwardClockYieldsZeroDt) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); behavior.update(robot::Time(1001.0)); behavior.update(robot::Time(1000.5)); // đồng hồ đi lùi EXPECT_NEAR(behavior.last_dt, 0.0, 1e-9); } TEST(RecoveryLifecycle, ElapsedTracksClockFromStart) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); const auto first = behavior.update(robot::Time(1001.25)); EXPECT_NEAR(first.elapsed, 1.25, 1e-6); EXPECT_NEAR(behavior.elapsed(), 1.25, 1e-6); const auto second = behavior.update(robot::Time(1004.0)); EXPECT_NEAR(second.elapsed, 4.0, 1e-6); } TEST(RecoveryLifecycle, RestartResetsElapsed) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(1000.0))); behavior.update(robot::Time(1005.0)); ASSERT_NEAR(behavior.elapsed(), 5.0, 1e-6); behavior.next_result = recovery_core::RecoveryResult::Running(); ASSERT_TRUE(behavior.start(RecoveryGoal(), robot::Time(2000.0))); EXPECT_NEAR(behavior.elapsed(), 0.0, 1e-9); const auto result = behavior.update(robot::Time(2000.5)); EXPECT_NEAR(result.elapsed, 0.5, 1e-6); } TEST(RecoveryLifecycle, GoalIsHandedToPluginVerbatim) { MockBehavior behavior(RecoveryOutputType::kNone); robot::NodeHandle nh; recovery_core::RecoveryContext ctx; ASSERT_TRUE(behavior.configure("mock", ctx, nh)); RecoveryGoal goal; goal.trigger = recovery_core::RecoveryTrigger::kOscillation; goal.angle = 1.5; goal.params["custom"] = 7.0; ASSERT_TRUE(behavior.start(goal, robot::Time(1000.0))); EXPECT_EQ(behavior.last_goal.trigger, recovery_core::RecoveryTrigger::kOscillation); ASSERT_TRUE(behavior.last_goal.angle.has_value()); EXPECT_DOUBLE_EQ(*behavior.last_goal.angle, 1.5); EXPECT_DOUBLE_EQ(behavior.last_goal.param("custom", 0.0), 7.0); } } // namespace int main(int argc, char** argv) { #ifdef RECOVERY_CORE_TEST_CONFIG_DIR setenv("PNKX_NAV_CORE_CONFIG_DIR", RECOVERY_CORE_TEST_CONFIG_DIR, 0); #endif testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }