optimal & fix file cmake
This commit is contained in:
405
test/mission_manager_test.cpp
Normal file
405
test/mission_manager_test.cpp
Normal file
@@ -0,0 +1,405 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <mission_adapters/mission_manager.h>
|
||||
|
||||
#include "mission_test_utils.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace mission_adapters;
|
||||
using mission_test::makeMission;
|
||||
using mission_test::makeMissions;
|
||||
|
||||
TEST(MissionManagerTest, ManagerRunsMissionLifecycle)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(1.0, 2.0)});
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
EXPECT_TRUE(manager.hasMission());
|
||||
|
||||
auto mission = manager.nextMission();
|
||||
ASSERT_NE(mission, nullptr);
|
||||
EXPECT_NE(mission->id, kInvalidMissionId) << "submit must return a MissionId";
|
||||
EXPECT_EQ(manager.state(), MissionState::RUNNING);
|
||||
|
||||
EXPECT_TRUE(manager.onNavigationDone(mission->id));
|
||||
EXPECT_EQ(manager.state(), MissionState::COMPLETED);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, ManagerHandlesPauseResumeCancelAndFailure)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(1.0, 1.0)});
|
||||
manager.pause();
|
||||
EXPECT_EQ(manager.state(), MissionState::PAUSED);
|
||||
|
||||
manager.resume();
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
|
||||
manager.cancel();
|
||||
EXPECT_EQ(manager.state(), MissionState::CANCELLED);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(2.0, 2.0)});
|
||||
const auto mission = manager.nextMission();
|
||||
ASSERT_NE(mission, nullptr);
|
||||
EXPECT_TRUE(manager.onNavigationFailed(mission->id));
|
||||
EXPECT_EQ(manager.state(), MissionState::FAILED);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, NavigationResultIsIgnoredOutsideRunningState)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(1.0, 1.0)});
|
||||
const MissionId queued_id = manager.nextMission()->id;
|
||||
|
||||
manager.emergency();
|
||||
EXPECT_FALSE(manager.onNavigationDone(queued_id));
|
||||
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
|
||||
|
||||
EXPECT_FALSE(manager.onNavigationFailed(queued_id));
|
||||
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
|
||||
|
||||
manager.clearEmergency();
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(2.0, 2.0)});
|
||||
manager.cancel();
|
||||
EXPECT_FALSE(manager.onNavigationDone(queued_id));
|
||||
EXPECT_EQ(manager.state(), MissionState::CANCELLED);
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, EmergencyClearsAndBlocksNewMissionsUntilCleared)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(1.0, 1.0)});
|
||||
manager.emergency();
|
||||
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(2.0, 2.0)});
|
||||
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
|
||||
manager.clearEmergency();
|
||||
manager.submit(std::vector<std::shared_ptr<Mission>>{makeMission(3.0, 3.0)});
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
EXPECT_TRUE(manager.hasMission());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// A1 — submit rỗng không được đụng vào hàng đợi đang chạy.
|
||||
//
|
||||
// Một order lỗi từ fleet manager (convert() trả rỗng) không phải là lệnh "huỷ mọi thứ". Nếu nó xoá
|
||||
// queue thì chặng đang chạy mất chủ: robot vẫn đi tới goal cũ vì không ai bảo navigation dừng, còn
|
||||
// mission layer thì kẹt ở RUNNING mà không còn mission nào để hoàn tất.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(MissionManagerTest, EmptySubmitLeavesRunningMissionUntouched)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(2));
|
||||
|
||||
const auto running = manager.nextMission();
|
||||
ASSERT_NE(running, nullptr);
|
||||
ASSERT_EQ(manager.state(), MissionState::RUNNING);
|
||||
|
||||
// Order lỗi -> convert() rỗng -> submit rỗng.
|
||||
manager.submit({});
|
||||
|
||||
EXPECT_EQ(manager.state(), MissionState::RUNNING);
|
||||
EXPECT_TRUE(manager.hasMission()) << "an empty submit wiped the running mission and the queue";
|
||||
EXPECT_EQ(manager.currentMissionId(), running->id) << "the running mission was replaced";
|
||||
EXPECT_EQ(manager.takePendingCancel(), kInvalidMissionId)
|
||||
<< "an empty submit must not ask navigation to stop";
|
||||
|
||||
// Chặng thứ hai vẫn phải còn trong hàng đợi.
|
||||
EXPECT_TRUE(manager.onNavigationDone(running->id));
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// A2 — outcome phải mang MissionId.
|
||||
//
|
||||
// Không có ID thì kết quả của chặng cũ (đến trễ, sau khi order đã bị thay) được ghi nhận cho chặng
|
||||
// mới: mission mới "hoàn thành" trong khi robot chưa hề chạy nó.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(MissionManagerTest, StaleOutcomeOfPreemptedMissionIsRejected)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
const auto first = manager.nextMission();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
// Order mới thay order cũ trong lúc chặng đầu đang chạy.
|
||||
manager.submit(makeMissions(1));
|
||||
const auto second = manager.nextMission();
|
||||
ASSERT_NE(second, nullptr);
|
||||
ASSERT_NE(first->id, second->id);
|
||||
|
||||
// Outcome của chặng ĐẦU về muộn, sau khi chặng hai đã bắt đầu.
|
||||
EXPECT_FALSE(manager.onNavigationDone(first->id));
|
||||
EXPECT_EQ(manager.state(), MissionState::RUNNING) << "a late outcome completed the wrong "
|
||||
"mission";
|
||||
EXPECT_EQ(manager.currentMissionId(), second->id);
|
||||
|
||||
// Outcome đúng của chặng hai vẫn được nhận bình thường.
|
||||
EXPECT_TRUE(manager.onNavigationDone(second->id));
|
||||
EXPECT_EQ(manager.state(), MissionState::COMPLETED);
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, MissionIdsAreUniqueAndMonotonic)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(3));
|
||||
|
||||
MissionId previous = kInvalidMissionId;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
const auto mission = manager.nextMission();
|
||||
ASSERT_NE(mission, nullptr) << "leg number " << i;
|
||||
EXPECT_GT(mission->id, previous);
|
||||
previous = mission->id;
|
||||
|
||||
ASSERT_TRUE(manager.onNavigationDone(mission->id));
|
||||
}
|
||||
|
||||
EXPECT_EQ(manager.state(), MissionState::COMPLETED);
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, RunningMissionIsDispatchedOnlyOnce)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(2));
|
||||
|
||||
const auto first = manager.nextMission();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
// M6: trong lúc RUNNING không có mission mới nào để giao.
|
||||
EXPECT_EQ(manager.nextMission(), nullptr);
|
||||
EXPECT_EQ(manager.nextMission(), nullptr);
|
||||
|
||||
ASSERT_TRUE(manager.onNavigationDone(first->id));
|
||||
const auto second = manager.nextMission();
|
||||
ASSERT_NE(second, nullptr);
|
||||
EXPECT_NE(second->id, first->id);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// A3 — mission layer phải có đường bảo navigation dừng.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(MissionManagerTest, CancelRequestsNavigationStopOfRunningMission)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(2));
|
||||
const auto running = manager.nextMission();
|
||||
ASSERT_NE(running, nullptr);
|
||||
|
||||
manager.cancel();
|
||||
|
||||
EXPECT_EQ(manager.takePendingCancel(), running->id);
|
||||
EXPECT_EQ(manager.takePendingCancel(), kInvalidMissionId) << "the stop request was taken twice";
|
||||
EXPECT_EQ(manager.state(), MissionState::CANCELLED);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, EmergencyRequestsNavigationStopOfRunningMission)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
const auto running = manager.nextMission();
|
||||
ASSERT_NE(running, nullptr);
|
||||
|
||||
manager.emergency();
|
||||
|
||||
EXPECT_EQ(manager.takePendingCancel(), running->id);
|
||||
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, PreemptRequestsNavigationStopOfReplacedMission)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
const auto first = manager.nextMission();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
|
||||
EXPECT_EQ(manager.takePendingCancel(), first->id)
|
||||
<< "a new order replaced the old one without telling navigation to stop the old leg";
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, SubmitWhilePausedKeepsRobotStopped)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
manager.pause();
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
|
||||
EXPECT_EQ(manager.state(), MissionState::PAUSED) << "a new order started running on its own "
|
||||
"while PAUSED";
|
||||
EXPECT_EQ(manager.nextMission(), nullptr);
|
||||
|
||||
manager.resume();
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
EXPECT_NE(manager.nextMission(), nullptr);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// M8 — một chặng hỏng thì phần còn lại của hàng đợi đi đâu.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(MissionManagerTest, FailureClearsRemainingQueueByDefault)
|
||||
{
|
||||
MissionManager manager; // clear_queue_on_failure mặc định = true
|
||||
|
||||
manager.submit(makeMissions(3));
|
||||
const auto first = manager.nextMission();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
ASSERT_TRUE(manager.onNavigationFailed(first->id));
|
||||
|
||||
// Tuyến tuần tự: không tới được chặng 1 thì chặng 2 nằm sau nó cũng không còn ý nghĩa.
|
||||
EXPECT_EQ(manager.state(), MissionState::FAILED);
|
||||
EXPECT_FALSE(manager.hasMission());
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, FailureKeepsQueueWhenConfigured)
|
||||
{
|
||||
MissionConfig config;
|
||||
config.clear_queue_on_failure = false; // các mission trong hàng đợi độc lập với nhau
|
||||
MissionManager manager(config);
|
||||
|
||||
manager.submit(makeMissions(3));
|
||||
const auto first = manager.nextMission();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
ASSERT_TRUE(manager.onNavigationFailed(first->id));
|
||||
|
||||
EXPECT_EQ(manager.state(), MissionState::QUEUED);
|
||||
EXPECT_TRUE(manager.hasMission());
|
||||
|
||||
const auto second = manager.nextMission();
|
||||
ASSERT_NE(second, nullptr);
|
||||
EXPECT_NE(second->id, first->id);
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, LastMissionFailureEndsInFailedEvenWhenQueueKept)
|
||||
{
|
||||
MissionConfig config;
|
||||
config.clear_queue_on_failure = false;
|
||||
MissionManager manager(config);
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
const auto only = manager.nextMission();
|
||||
ASSERT_NE(only, nullptr);
|
||||
|
||||
ASSERT_TRUE(manager.onNavigationFailed(only->id));
|
||||
EXPECT_EQ(manager.state(), MissionState::FAILED);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// M7 — mission_timeout là lưới cuối khi navigation không bao giờ báo kết quả về.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST(MissionManagerTest, TimeoutDisabledByDefaultLetsMissionRunOn)
|
||||
{
|
||||
MissionManager manager;
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
ASSERT_NE(manager.nextMission(), nullptr);
|
||||
|
||||
// waitForWork phải chặn (không có việc); wakeUp là đường ra duy nhất.
|
||||
std::thread waker([&manager] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(80));
|
||||
manager.wakeUp();
|
||||
});
|
||||
|
||||
EXPECT_FALSE(manager.waitForWork());
|
||||
waker.join();
|
||||
|
||||
EXPECT_EQ(manager.state(), MissionState::RUNNING) << "the mission was cancelled although the "
|
||||
"timeout is off";
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, TimeoutFailsMissionAndRequestsNavigationStop)
|
||||
{
|
||||
MissionConfig config;
|
||||
config.mission_timeout = 0.15; // [s]
|
||||
MissionManager manager(config);
|
||||
|
||||
manager.submit(makeMissions(1));
|
||||
const auto running = manager.nextMission();
|
||||
ASSERT_NE(running, nullptr);
|
||||
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
|
||||
// waitForWork tự thức dậy đúng lúc hết hạn và biến quá hạn thành việc cần làm.
|
||||
ASSERT_TRUE(manager.waitForWork());
|
||||
|
||||
const double elapsed =
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
|
||||
EXPECT_GE(elapsed, 0.15) << "the leg was cancelled before it expired";
|
||||
|
||||
EXPECT_EQ(manager.state(), MissionState::FAILED);
|
||||
EXPECT_EQ(manager.takePendingCancel(), running->id)
|
||||
<< "it expired but navigation was not told to stop — the robot keeps driving to the old "
|
||||
"goal";
|
||||
}
|
||||
|
||||
TEST(MissionManagerTest, TimeoutIsMeasuredPerMissionNotPerQueue)
|
||||
{
|
||||
MissionConfig config;
|
||||
config.mission_timeout = 0.2; // [s]
|
||||
config.clear_queue_on_failure = false;
|
||||
MissionManager manager(config);
|
||||
|
||||
manager.submit(makeMissions(2));
|
||||
|
||||
const auto first = manager.nextMission();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
// Chặng đầu xong sớm; mốc thời gian phải được gieo lại cho chặng hai.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
||||
ASSERT_TRUE(manager.onNavigationDone(first->id));
|
||||
|
||||
const auto second = manager.nextMission();
|
||||
ASSERT_NE(second, nullptr);
|
||||
|
||||
// Nếu mốc không được gieo lại, chặng hai sẽ hết hạn gần như tức thì.
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
ASSERT_TRUE(manager.waitForWork());
|
||||
const double elapsed =
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
|
||||
|
||||
EXPECT_GE(elapsed, 0.19) << "the second leg inherited the clock of the first one";
|
||||
EXPECT_EQ(manager.takePendingCancel(), second->id);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in New Issue
Block a user