add file test

This commit is contained in:
2026-06-29 13:45:50 +07:00
parent 829ce20fbc
commit 33ee9b7f31
7 changed files with 469 additions and 638 deletions

View File

@@ -92,7 +92,7 @@ else()
catkin_package( catkin_package(
INCLUDE_DIRS include INCLUDE_DIRS include
LIBRARIES ${PROJECT_NAME} ${PROJECT_NAME}_utils LIBRARIES ${PROJECT_NAME}
CATKIN_DEPENDS CATKIN_DEPENDS
robot_costmap_2d robot_costmap_2d
robot_nav_core robot_nav_core
@@ -105,7 +105,7 @@ else()
robot_visualization_msgs robot_visualization_msgs
robot_nav_2d_utils robot_nav_2d_utils
data_convert data_convert
DEPENDS PCL Boost Eigen DEPENDS PCL Boost
) )
include_directories( include_directories(
@@ -218,11 +218,11 @@ endif()
# ======================================================== # ========================================================
if(BUILDING_WITH_CATKIN) if(BUILDING_WITH_CATKIN)
# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_utils install(TARGETS ${PROJECT_NAME}
# ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
# RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION} RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}
# ) )
install(DIRECTORY include/${PROJECT_NAME}/ install(DIRECTORY include/${PROJECT_NAME}/
DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
@@ -237,12 +237,12 @@ if(BUILDING_WITH_CATKIN)
else() else()
# install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_utils install(TARGETS ${PROJECT_NAME}
# EXPORT ${PROJECT_NAME}-targets EXPORT ${PROJECT_NAME}-targets
# ARCHIVE DESTINATION lib ARCHIVE DESTINATION lib
# LIBRARY DESTINATION lib LIBRARY DESTINATION lib
# RUNTIME DESTINATION bin RUNTIME DESTINATION bin
# ) )
install(EXPORT ${PROJECT_NAME}-targets install(EXPORT ${PROJECT_NAME}-targets
FILE ${PROJECT_NAME}-targets.cmake FILE ${PROJECT_NAME}-targets.cmake
@@ -290,24 +290,15 @@ if(CATKIN_ENABLE_TESTING)
message(FATAL_ERROR "catkin_add_gtest NOT FOUND") message(FATAL_ERROR "catkin_add_gtest NOT FOUND")
endif() endif()
find_package(GTest REQUIRED) catkin_add_gtest(test_mission_adapters
test/mission_adapters_test.cpp
add_executable(test_mission_adapters
src/test_mission_adapters.cpp
)
target_link_libraries(test_mission_adapters
mission_adapters
GTest::GTest
GTest::Main
pthread
${catkin_LIBRARIES}
) )
if(TARGET test_mission_adapters) if(TARGET test_mission_adapters)
target_link_libraries(test_mission_adapters target_link_libraries(test_mission_adapters
mission_adapters mission_adapters
${catkin_LIBRARIES} ${catkin_LIBRARIES}
pthread
) )
target_include_directories(test_mission_adapters PRIVATE target_include_directories(test_mission_adapters PRIVATE

View File

@@ -7,8 +7,10 @@
#include <memory> #include <memory>
#include <thread> #include <thread>
#include <mutex> #include <mutex>
#include <condition_variable>
#include <functional> #include <functional>
#include <atomic> #include <atomic>
#include <cstdint>
#include <robot/robot.h> #include <robot/robot.h>
#include <robot_protocol_msgs/Order.h> #include <robot_protocol_msgs/Order.h>
@@ -68,17 +70,17 @@ namespace mission_adapters
class Action class Action
{ {
public: public:
int sequenceId; int sequenceId = 0;
ActionType type; ActionType type = ActionType::NODE_ACTION;
robot_protocol_msgs::Action action; robot_protocol_msgs::Action action;
}; };
class Mission class Mission
{ {
public: public:
int sequenceId; int sequenceId = 0;
MissionType type; MissionType type = MissionType::SIMPLE_GOAL;
int priority; int priority = 0;
robot_geometry_msgs::PoseStamped start; robot_geometry_msgs::PoseStamped start;
robot_geometry_msgs::PoseStamped goal; robot_geometry_msgs::PoseStamped goal;
std::vector<robot_protocol_msgs::Node> nodes; std::vector<robot_protocol_msgs::Node> nodes;
@@ -88,8 +90,9 @@ namespace mission_adapters
struct Event struct Event
{ {
EventType type; EventType type = EventType::SUBMIT_MISSIONS;
int priority; int priority = PRIORITY_ORDER;
uint64_t sequence = 0;
std::vector<std::shared_ptr<Mission>> missions; std::vector<std::shared_ptr<Mission>> missions;
}; };
@@ -97,7 +100,10 @@ namespace mission_adapters
{ {
bool operator()(const Event& lhs, const Event& rhs) const bool operator()(const Event& lhs, const Event& rhs) const
{ {
if (lhs.priority != rhs.priority)
return lhs.priority > rhs.priority; return lhs.priority > rhs.priority;
return lhs.sequence > rhs.sequence;
} }
}; };
@@ -108,6 +114,7 @@ namespace mission_adapters
std::priority_queue<Event, std::vector<Event>, EventCompare> queue_; std::priority_queue<Event, std::vector<Event>, EventCompare> queue_;
std::condition_variable cv_; std::condition_variable cv_;
bool stop_ = false; bool stop_ = false;
uint64_t next_sequence_ = 0;
public: public:
void push(const Event& event); void push(const Event& event);

View File

@@ -11,13 +11,16 @@ namespace mission_adapters
void EventBus::push(const Event& event) void EventBus::push(const Event& event)
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
queue_.push(event); Event queued_event = event;
queued_event.sequence = next_sequence_++;
queue_.push(std::move(queued_event));
cv_.notify_one(); cv_.notify_one();
} }
void EventBus::push(Event&& event) void EventBus::push(Event&& event)
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
event.sequence = next_sequence_++;
queue_.push(std::move(event)); queue_.push(std::move(event));
cv_.notify_one(); cv_.notify_one();
} }
@@ -47,6 +50,8 @@ namespace mission_adapters
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
stop_ = false; stop_ = false;
next_sequence_ = 0;
while (!queue_.empty()) queue_.pop();
} }
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
@@ -76,6 +81,12 @@ namespace mission_adapters
if (order.nodes.empty()) return missions; if (order.nodes.empty()) return missions;
if (order.nodes.size() > 1 && order.edges.size() < order.nodes.size() - 1)
{
robot::log_error("Invalid VDA5050 order: edge count is smaller than node_count - 1");
return missions;
}
for (size_t i = 0; i < order.nodes.size(); ++i) for (size_t i = 0; i < order.nodes.size(); ++i)
{ {
if (!order.nodes[i].actions.empty()) if (!order.nodes[i].actions.empty())
@@ -169,6 +180,10 @@ namespace mission_adapters
void MissionManager::submit(const std::vector<std::shared_ptr<Mission>>& missions) void MissionManager::submit(const std::vector<std::shared_ptr<Mission>>& missions)
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (state_ == MissionState::EMERGENCY)
return;
if (!mission_queue_.empty()) if (!mission_queue_.empty())
{ {
mission_queue_ = {}; mission_queue_ = {};
@@ -188,7 +203,6 @@ namespace mission_adapters
case MissionState::COMPLETED: case MissionState::COMPLETED:
case MissionState::FAILED: case MissionState::FAILED:
case MissionState::CANCELLED: case MissionState::CANCELLED:
case MissionState::EMERGENCY:
case MissionState::CLEAR_EMERGENCY: case MissionState::CLEAR_EMERGENCY:
state_ = MissionState::QUEUED; state_ = MissionState::QUEUED;
break; break;
@@ -230,6 +244,10 @@ namespace mission_adapters
void MissionManager::onNavigationDone() void MissionManager::onNavigationDone()
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (state_ != MissionState::RUNNING)
return;
current_mission_.reset(); current_mission_.reset();
state_ = mission_queue_.empty() ? MissionState::IDLE : MissionState::QUEUED; state_ = mission_queue_.empty() ? MissionState::IDLE : MissionState::QUEUED;
} }
@@ -237,6 +255,10 @@ namespace mission_adapters
void MissionManager::onNavigationFailed() void MissionManager::onNavigationFailed()
{ {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
if (state_ != MissionState::RUNNING)
return;
current_mission_.reset(); current_mission_.reset();
while (!mission_queue_.empty()) mission_queue_.pop(); while (!mission_queue_.empty()) mission_queue_.pop();
state_ = MissionState::FAILED; state_ = MissionState::FAILED;
@@ -374,38 +396,59 @@ namespace mission_adapters
void EventProcessor::pauseEvent() void EventProcessor::pauseEvent()
{ {
event_bus_.push({EventType::PAUSE, PRIORITY_PAUSE, {}}); Event event;
event.type = EventType::PAUSE;
event.priority = PRIORITY_PAUSE;
event_bus_.push(std::move(event));
} }
void EventProcessor::resumeEvent() void EventProcessor::resumeEvent()
{ {
event_bus_.push({EventType::RESUME, PRIORITY_RESUME, {}}); Event event;
event.type = EventType::RESUME;
event.priority = PRIORITY_RESUME;
event_bus_.push(std::move(event));
} }
void EventProcessor::cancelEvent() void EventProcessor::cancelEvent()
{ {
event_bus_.push({EventType::CANCEL, PRIORITY_CANCEL, {}}); Event event;
event.type = EventType::CANCEL;
event.priority = PRIORITY_CANCEL;
event_bus_.push(std::move(event));
} }
void EventProcessor::navDoneEvent() void EventProcessor::navDoneEvent()
{ {
event_bus_.push({EventType::NAV_DONE, PRIORITY_NAV_DONE, {}}); Event event;
event.type = EventType::NAV_DONE;
event.priority = PRIORITY_NAV_DONE;
event_bus_.push(std::move(event));
} }
// FIX #4: Use PRIORITY_NAV_FAILED (correct constant name). // FIX #4: Use PRIORITY_NAV_FAILED (correct constant name).
void EventProcessor::navFailedEvent() void EventProcessor::navFailedEvent()
{ {
event_bus_.push({EventType::NAV_FAILED, PRIORITY_NAV_FAILED, {}}); Event event;
event.type = EventType::NAV_FAILED;
event.priority = PRIORITY_NAV_FAILED;
event_bus_.push(std::move(event));
} }
void EventProcessor::emergencyEvent() void EventProcessor::emergencyEvent()
{ {
event_bus_.push({EventType::EMERGENCY, PRIORITY_EMERGENCY, {}}); Event event;
event.type = EventType::EMERGENCY;
event.priority = PRIORITY_EMERGENCY;
event_bus_.push(std::move(event));
} }
void EventProcessor::clearEmergencyEvent() void EventProcessor::clearEmergencyEvent()
{ {
event_bus_.push({EventType::CLEAR_EMERGENCY, PRIORITY_EMERGENCY, {}}); Event event;
event.type = EventType::CLEAR_EMERGENCY;
event.priority = PRIORITY_EMERGENCY;
event_bus_.push(std::move(event));
} }
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────

View File

@@ -1,6 +1,8 @@
#include <mission_adapters/mission_adapters.h> #include <mission_adapters/mission_adapters.h>
#include <move_base_core/navigation.h> #include <move_base_core/navigation.h>
#include <utility>
using namespace mission_adapters; using namespace mission_adapters;
class RobotControlTest class RobotControlTest
@@ -23,7 +25,7 @@ class RobotControlTest
} }
public: public:
RobotControlTest(); explicit RobotControlTest(robot::move_base_core::BaseNavigation::Ptr move_base);
~RobotControlTest(); ~RobotControlTest();
void run(); void run();
@@ -32,7 +34,8 @@ private:
void executeMission(const Mission& mission); void executeMission(const Mission& mission);
}; };
RobotControlTest::RobotControlTest() RobotControlTest::RobotControlTest(robot::move_base_core::BaseNavigation::Ptr move_base)
: move_base_ptr_(std::move(move_base))
{} {}
RobotControlTest::~RobotControlTest() RobotControlTest::~RobotControlTest()
@@ -43,27 +46,39 @@ RobotControlTest::~RobotControlTest()
void RobotControlTest::run() void RobotControlTest::run()
{ {
if (!move_base_ptr_)
{
robot::log_error("RobotControlTest requires a valid BaseNavigation pointer");
return;
}
robot::Rate rate(50); robot::Rate rate(50);
// FIX #1: Set the callback ONCE before starting the executor, not inside the loop.
mission_executor_.setMissionCallback( mission_executor_.setMissionCallback(
[this](const std::shared_ptr<Mission>& mission) [this](const std::shared_ptr<Mission>& mission)
{ {
executeMission(*mission); executeMission(*mission);
}); });
event_processor_.start();
mission_executor_.start();
while (robot::ok())
while (true)
{ {
auto feedback = move_base_ptr_->getFeedback(); auto feedback = move_base_ptr_->getFeedback();
if (!feedback)
{
rate.sleep();
continue;
}
auto nav_state = feedback->navigation_state; auto nav_state = feedback->navigation_state;
if (nav_state != prev_nav_done_state_ && nav_state == robot::move_base_core::State::SUCCEEDED && areActionsDone()) if (nav_state != prev_nav_state_ && nav_state == robot::move_base_core::State::SUCCEEDED && areActionsDone())
{ {
event_processor_.navDoneEvent(); event_processor_.navDoneEvent();
} }
else if (nav_state == robot::move_base_core::State::ABORTED) else if (nav_state != prev_nav_state_ && nav_state == robot::move_base_core::State::ABORTED)
{ {
event_processor_.navFailedEvent(); event_processor_.navFailedEvent();
} }
@@ -86,10 +101,3 @@ void RobotControlTest::executeMission(const Mission& mission)
// TODO: send mission goal to move_base_ptr_ // TODO: send mission goal to move_base_ptr_
(void)mission; (void)mission;
} }
int main()
{
RobotControlTest test;
test.run();
return 0;
}

Binary file not shown.

View File

@@ -1,578 +0,0 @@
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <thread>
#include "mission_adapters/mission_adapters.h"
using namespace mission_adapters;
namespace
{
robot_geometry_msgs::PoseStamped MakeGoal(double x, double y)
{
robot_geometry_msgs::PoseStamped g;
g.pose.position.x = x;
g.pose.position.y = y;
return g;
}
robot_protocol_msgs::Action MakeAction(
const std::string &id)
{
robot_protocol_msgs::Action a;
a.actionId = id;
a.actionType = "TEST";
return a;
}
robot_protocol_msgs::Node MakeNode(
int seq,
bool add_action = false)
{
robot_protocol_msgs::Node n;
n.sequenceId = seq;
n.nodeId = "node_" + std::to_string(seq);
n.nodePosition.x = seq;
n.nodePosition.y = seq;
if (add_action)
n.actions.push_back(
MakeAction("node_action_" + std::to_string(seq)));
return n;
}
robot_protocol_msgs::Edge MakeEdge(
int seq,
bool add_action = false)
{
robot_protocol_msgs::Edge e;
e.sequenceId = seq;
e.edgeId = "edge_" + std::to_string(seq);
if (add_action)
e.actions.push_back(
MakeAction("edge_action_" + std::to_string(seq)));
return e;
}
robot_protocol_msgs::Order MakeOrder(
int node_count)
{
robot_protocol_msgs::Order order;
for (int i = 0; i < node_count; ++i)
order.nodes.push_back(
MakeNode(i));
for (int i = 0; i < node_count - 1; ++i)
order.edges.push_back(
MakeEdge(i));
return order;
}
} // namespace
//--------------------------------------------------------------
// EventBus
//--------------------------------------------------------------
TEST(EventBus, PriorityOrdering)
{
EventBus bus;
bus.push({EventType::PAUSE,
PRIORITY_PAUSE,
{}});
bus.push({EventType::CANCEL,
PRIORITY_CANCEL,
{}});
bus.push({EventType::EMERGENCY,
PRIORITY_EMERGENCY,
{}});
Event e;
ASSERT_TRUE(bus.pop(e));
EXPECT_EQ(e.priority,
PRIORITY_EMERGENCY);
ASSERT_TRUE(bus.pop(e));
EXPECT_EQ(e.priority,
PRIORITY_CANCEL);
ASSERT_TRUE(bus.pop(e));
EXPECT_EQ(e.priority,
PRIORITY_PAUSE);
}
TEST(EventBus, StopReturnsFalse)
{
EventBus bus;
std::thread t([&]()
{ bus.stop(); });
Event e;
EXPECT_FALSE(bus.pop(e));
t.join();
}
TEST(EventBus, ConcurrentPush)
{
EventBus bus;
constexpr int N = 100;
std::thread producer([&]()
{
for (int i = 0; i < N; i++)
{
bus.push(
{EventType::NAV_DONE,
PRIORITY_NAV_DONE,
{}});
}
});
int count = 0;
while (count < N)
{
Event e;
if (bus.pop(e))
++count;
}
producer.join();
EXPECT_EQ(count, N);
}
//--------------------------------------------------------------
// GoalAdapter
//--------------------------------------------------------------
TEST(GoalAdapter, ConvertSingleGoal)
{
GoalAdapter adapter;
auto missions =
adapter.convert(
MakeGoal(5.5, 9.1));
ASSERT_EQ(missions.size(), 1u);
EXPECT_EQ(
missions[0]->type,
MissionType::SIMPLE_GOAL);
EXPECT_DOUBLE_EQ(
missions[0]->goal.pose.position.x,
5.5);
EXPECT_DOUBLE_EQ(
missions[0]->goal.pose.position.y,
9.1);
}
//--------------------------------------------------------------
// VDA5050Adapter
//--------------------------------------------------------------
TEST(VDA5050Adapter, EmptyOrder)
{
VDA5050Adapter adapter;
robot_protocol_msgs::Order order;
auto missions =
adapter.convert(order);
EXPECT_TRUE(
missions.empty());
}
TEST(VDA5050Adapter, OrderWithoutActionsCreatesTailMission)
{
VDA5050Adapter adapter;
auto order =
MakeOrder(4);
auto missions =
adapter.convert(order);
ASSERT_EQ(
missions.size(),
1u);
EXPECT_EQ(
missions[0]->nodes.size(),
4u);
EXPECT_EQ(
missions[0]->edges.size(),
3u);
}
TEST(VDA5050Adapter, SplitAtNodeAction)
{
VDA5050Adapter adapter;
auto order =
MakeOrder(5);
order.nodes[2].actions.push_back(
MakeAction("dock"));
auto missions =
adapter.convert(order);
ASSERT_EQ(
missions.size(),
2u);
EXPECT_EQ(
missions[0]->nodes.size(),
3u);
EXPECT_EQ(
missions[1]->nodes.size(),
3u);
}
TEST(VDA5050Adapter, CollectEdgeAction)
{
VDA5050Adapter adapter;
auto order =
MakeOrder(2);
order.nodes[1].actions.push_back(
MakeAction("node"));
order.edges[0].actions.push_back(
MakeAction("edge"));
auto missions =
adapter.convert(order);
ASSERT_EQ(
missions.size(),
1u);
ASSERT_EQ(
missions[0]->actions.size(),
2u);
EXPECT_LT(
missions[0]->actions[0].sequenceId,
missions[0]->actions[1].sequenceId);
}
//--------------------------------------------------------------
// MissionManager
//--------------------------------------------------------------
TEST(MissionManager, SubmitChangesState)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 2)));
EXPECT_EQ(
mgr.state(),
MissionState::QUEUED);
EXPECT_TRUE(
mgr.hasMission());
}
TEST(MissionManager, NextMission)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 2)));
auto m =
mgr.nextMission();
ASSERT_NE(
m,
nullptr);
EXPECT_EQ(
mgr.state(),
MissionState::RUNNING);
}
TEST(MissionManager, NavigationDone)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(0, 0)));
mgr.nextMission();
mgr.onNavigationDone();
EXPECT_EQ(
mgr.state(),
MissionState::IDLE);
EXPECT_FALSE(
mgr.hasMission());
}
TEST(MissionManager, NavigationFailed)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(0, 0)));
mgr.nextMission();
mgr.onNavigationFailed();
EXPECT_EQ(
mgr.state(),
MissionState::FAILED);
}
TEST(MissionManager, PauseResume)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 1)));
mgr.pause();
EXPECT_EQ(
mgr.state(),
MissionState::PAUSED);
mgr.resume();
EXPECT_EQ(
mgr.state(),
MissionState::QUEUED);
}
TEST(MissionManager, Cancel)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 1)));
mgr.cancel();
EXPECT_EQ(
mgr.state(),
MissionState::CANCELLED);
EXPECT_FALSE(
mgr.hasMission());
}
TEST(MissionManager, Emergency)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(2, 2)));
mgr.emergency();
EXPECT_EQ(
mgr.state(),
MissionState::EMERGENCY);
}
TEST(MissionManager, ClearEmergency)
{
MissionManager mgr;
mgr.emergency();
mgr.clearEmergency();
EXPECT_EQ(
mgr.state(),
MissionState::CLEAR_EMERGENCY);
}
//--------------------------------------------------------------
// EventProcessor
//--------------------------------------------------------------
TEST(EventProcessor, GoalEvent)
{
MissionManager mgr;
EventProcessor proc(mgr);
proc.start();
proc.goalEvent(
MakeGoal(5, 6));
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
EXPECT_EQ(
mgr.state(),
MissionState::QUEUED);
proc.stop();
}
TEST(EventProcessor, EmergencyHasHighestPriority)
{
MissionManager mgr;
EventProcessor proc(mgr);
proc.start();
proc.goalEvent(
MakeGoal(1, 1));
proc.emergencyEvent();
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
EXPECT_EQ(
mgr.state(),
MissionState::EMERGENCY);
proc.stop();
}
//--------------------------------------------------------------
// MissionExecutor
//--------------------------------------------------------------
TEST(MissionExecutor, CallbackInvokedOnce)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(10, 10)));
MissionExecutor exec(mgr);
std::atomic<int> count{0};
exec.setMissionCallback(
[&](const std::shared_ptr<Mission> &)
{
count++;
});
exec.start();
std::this_thread::sleep_for(
std::chrono::milliseconds(200));
exec.stop();
EXPECT_EQ(
count.load(),
1);
}
TEST(MissionExecutor, NewMissionAfterDone)
{
MissionManager mgr;
GoalAdapter adapter;
mgr.submit(
adapter.convert(
MakeGoal(1, 1)));
MissionExecutor exec(mgr);
std::atomic<int> count{0};
exec.setMissionCallback(
[&](const std::shared_ptr<Mission> &)
{
count++;
});
exec.start();
std::this_thread::sleep_for(
std::chrono::milliseconds(100));
mgr.onNavigationDone();
mgr.submit(
adapter.convert(
MakeGoal(2, 2)));
std::this_thread::sleep_for(
std::chrono::milliseconds(150));
exec.stop();
EXPECT_EQ(
count.load(),
2);
}
//--------------------------------------------------------------
int main(int argc, char **argv)
{
::testing::InitGoogleTest(
&argc,
argv);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,360 @@
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <mission_adapters/mission_adapters.h>
namespace
{
using namespace mission_adapters;
robot_geometry_msgs::PoseStamped makeGoal(double x, double y)
{
robot_geometry_msgs::PoseStamped goal;
goal.pose.position.x = x;
goal.pose.position.y = y;
return goal;
}
robot_protocol_msgs::Action makeAction(const std::string& id)
{
robot_protocol_msgs::Action action;
action.actionId = id;
action.actionType = "TEST";
return action;
}
robot_protocol_msgs::Node makeNode(int sequence_id, bool add_action = false)
{
robot_protocol_msgs::Node node;
node.sequenceId = sequence_id;
node.nodeId = "node_" + std::to_string(sequence_id);
node.nodePosition.x = sequence_id;
node.nodePosition.y = sequence_id;
if (add_action)
node.actions.push_back(makeAction("node_action_" + std::to_string(sequence_id)));
return node;
}
robot_protocol_msgs::Edge makeEdge(int sequence_id, bool add_action = false)
{
robot_protocol_msgs::Edge edge;
edge.sequenceId = sequence_id;
edge.edgeId = "edge_" + std::to_string(sequence_id);
if (add_action)
edge.actions.push_back(makeAction("edge_action_" + std::to_string(sequence_id)));
return edge;
}
robot_protocol_msgs::Order makeOrder(int node_count)
{
robot_protocol_msgs::Order order;
for (int i = 0; i < node_count; ++i)
order.nodes.push_back(makeNode(i));
for (int i = 0; i < node_count - 1; ++i)
order.edges.push_back(makeEdge(i));
return order;
}
bool waitForState(MissionManager& manager, MissionState expected, std::chrono::milliseconds timeout)
{
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline)
{
if (manager.state() == expected)
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return manager.state() == expected;
}
class MissionAdaptersTest : public ::testing::Test
{
protected:
GoalAdapter goal_adapter;
VDA5050Adapter order_adapter;
};
TEST(EventBusTest, PopsHighestPriorityFirst)
{
EventBus bus;
Event pause;
pause.type = EventType::PAUSE;
pause.priority = PRIORITY_PAUSE;
bus.push(pause);
Event cancel;
cancel.type = EventType::CANCEL;
cancel.priority = PRIORITY_CANCEL;
bus.push(cancel);
Event emergency;
emergency.type = EventType::EMERGENCY;
emergency.priority = PRIORITY_EMERGENCY;
bus.push(emergency);
Event event;
ASSERT_TRUE(bus.pop(event));
EXPECT_EQ(event.type, EventType::EMERGENCY);
ASSERT_TRUE(bus.pop(event));
EXPECT_EQ(event.type, EventType::CANCEL);
ASSERT_TRUE(bus.pop(event));
EXPECT_EQ(event.type, EventType::PAUSE);
}
TEST(EventBusTest, PreservesFifoOrderForSamePriority)
{
EventBus bus;
Event emergency;
emergency.type = EventType::EMERGENCY;
emergency.priority = PRIORITY_EMERGENCY;
bus.push(emergency);
Event clear_emergency;
clear_emergency.type = EventType::CLEAR_EMERGENCY;
clear_emergency.priority = PRIORITY_EMERGENCY;
bus.push(clear_emergency);
Event event;
ASSERT_TRUE(bus.pop(event));
EXPECT_EQ(event.type, EventType::EMERGENCY);
ASSERT_TRUE(bus.pop(event));
EXPECT_EQ(event.type, EventType::CLEAR_EMERGENCY);
}
TEST(EventBusTest, StopUnblocksPop)
{
EventBus bus;
Event event;
std::thread stopper([&bus] { bus.stop(); });
EXPECT_FALSE(bus.pop(event));
stopper.join();
}
TEST(EventBusTest, ResetDropsPendingEvents)
{
EventBus bus;
Event pause;
pause.type = EventType::PAUSE;
pause.priority = PRIORITY_PAUSE;
bus.push(pause);
bus.reset();
EXPECT_TRUE(bus.empty());
}
TEST_F(MissionAdaptersTest, GoalAdapterCreatesSingleMission)
{
const auto missions = goal_adapter.convert(makeGoal(5.5, 9.1));
ASSERT_EQ(missions.size(), 1u);
EXPECT_EQ(missions.front()->type, MissionType::SIMPLE_GOAL);
EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.x, 5.5);
EXPECT_DOUBLE_EQ(missions.front()->goal.pose.position.y, 9.1);
}
TEST_F(MissionAdaptersTest, EmptyOrderCreatesNoMissions)
{
robot_protocol_msgs::Order order;
EXPECT_TRUE(order_adapter.convert(order).empty());
}
TEST_F(MissionAdaptersTest, OrderWithoutActionsCreatesOneTailMission)
{
const auto missions = order_adapter.convert(makeOrder(4));
ASSERT_EQ(missions.size(), 1u);
EXPECT_EQ(missions.front()->nodes.size(), 4u);
EXPECT_EQ(missions.front()->edges.size(), 3u);
}
TEST_F(MissionAdaptersTest, InvalidOrderWithMissingEdgesCreatesNoMissions)
{
auto order = makeOrder(4);
order.edges.pop_back();
EXPECT_TRUE(order_adapter.convert(order).empty());
}
TEST_F(MissionAdaptersTest, OrderSplitsAtNodeAction)
{
auto order = makeOrder(5);
order.nodes[2].actions.push_back(makeAction("dock"));
const auto missions = order_adapter.convert(order);
ASSERT_EQ(missions.size(), 2u);
EXPECT_EQ(missions[0]->nodes.size(), 3u);
EXPECT_EQ(missions[1]->nodes.size(), 3u);
}
TEST_F(MissionAdaptersTest, OrderCollectsAndSortsActions)
{
auto order = makeOrder(2);
order.edges[0].actions.push_back(makeAction("edge"));
order.nodes[1].actions.push_back(makeAction("node"));
const auto missions = order_adapter.convert(order);
ASSERT_EQ(missions.size(), 1u);
ASSERT_EQ(missions.front()->actions.size(), 2u);
EXPECT_EQ(missions.front()->actions[0].type, ActionType::EDGE_ACTION);
EXPECT_EQ(missions.front()->actions[1].type, ActionType::NODE_ACTION);
}
TEST_F(MissionAdaptersTest, ManagerRunsMissionLifecycle)
{
MissionManager manager;
manager.submit(goal_adapter.convert(makeGoal(1.0, 2.0)));
EXPECT_EQ(manager.state(), MissionState::QUEUED);
EXPECT_TRUE(manager.hasMission());
auto mission = manager.nextMission();
ASSERT_NE(mission, nullptr);
EXPECT_EQ(manager.state(), MissionState::RUNNING);
manager.onNavigationDone();
EXPECT_EQ(manager.state(), MissionState::IDLE);
EXPECT_FALSE(manager.hasMission());
}
TEST_F(MissionAdaptersTest, ManagerHandlesPauseResumeCancelAndFailure)
{
MissionManager manager;
manager.submit(goal_adapter.convert(makeGoal(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(goal_adapter.convert(makeGoal(2.0, 2.0)));
manager.nextMission();
manager.onNavigationFailed();
EXPECT_EQ(manager.state(), MissionState::FAILED);
EXPECT_FALSE(manager.hasMission());
}
TEST_F(MissionAdaptersTest, NavigationResultIsIgnoredOutsideRunningState)
{
MissionManager manager;
manager.submit(goal_adapter.convert(makeGoal(1.0, 1.0)));
manager.emergency();
manager.onNavigationDone();
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
manager.onNavigationFailed();
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
manager.clearEmergency();
manager.submit(goal_adapter.convert(makeGoal(2.0, 2.0)));
manager.cancel();
manager.onNavigationDone();
EXPECT_EQ(manager.state(), MissionState::CANCELLED);
}
TEST_F(MissionAdaptersTest, EmergencyClearsAndBlocksNewMissionsUntilCleared)
{
MissionManager manager;
manager.submit(goal_adapter.convert(makeGoal(1.0, 1.0)));
manager.emergency();
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
EXPECT_FALSE(manager.hasMission());
manager.submit(goal_adapter.convert(makeGoal(2.0, 2.0)));
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
EXPECT_FALSE(manager.hasMission());
manager.clearEmergency();
manager.submit(goal_adapter.convert(makeGoal(3.0, 3.0)));
EXPECT_EQ(manager.state(), MissionState::QUEUED);
EXPECT_TRUE(manager.hasMission());
}
TEST_F(MissionAdaptersTest, EventProcessorProcessesGoalAndEmergency)
{
MissionManager manager;
EventProcessor processor(manager);
processor.start();
processor.goalEvent(makeGoal(5.0, 6.0));
EXPECT_TRUE(waitForState(manager, MissionState::QUEUED, std::chrono::milliseconds(250)));
processor.emergencyEvent();
EXPECT_TRUE(waitForState(manager, MissionState::EMERGENCY, std::chrono::milliseconds(250)));
processor.goalEvent(makeGoal(7.0, 8.0));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
EXPECT_EQ(manager.state(), MissionState::EMERGENCY);
processor.clearEmergencyEvent();
EXPECT_TRUE(waitForState(manager, MissionState::CLEAR_EMERGENCY, std::chrono::milliseconds(250)));
processor.stop();
}
TEST_F(MissionAdaptersTest, MissionExecutorDispatchesEachMissionOnce)
{
MissionManager manager;
MissionExecutor executor(manager);
std::atomic<int> callback_count{0};
executor.setMissionCallback(
[&callback_count](const std::shared_ptr<Mission>& mission)
{
ASSERT_NE(mission, nullptr);
++callback_count;
});
manager.submit(goal_adapter.convert(makeGoal(10.0, 10.0)));
executor.start();
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(250);
while (callback_count.load() < 1 && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
manager.onNavigationDone();
manager.submit(goal_adapter.convert(makeGoal(11.0, 11.0)));
const auto second_deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(250);
while (callback_count.load() < 2 && std::chrono::steady_clock::now() < second_deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(5));
executor.stop();
EXPECT_EQ(callback_count.load(), 2);
}
} // namespace
int main(int argc, char** argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}