// Copyright (c) 2021 RoboTech Vision // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Reserved. #include #include #include #include #include #include #include #include #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" #include "rclcpp_action/rclcpp_action.hpp" #include "nav2_core/smoother.hpp" #include "nav2_core/exceptions.hpp" #include "nav2_msgs/action/smooth_path.hpp" #include "nav2_smoother/nav2_smoother.hpp" #include "tf2_ros/create_timer_ros.h" using SmoothAction = nav2_msgs::action::SmoothPath; using ClientGoalHandle = rclcpp_action::ClientGoalHandle; using namespace std::chrono_literals; // A smoother for testing the base class class DummySmoother : public nav2_core::Smoother { public: DummySmoother() {} ~DummySmoother() {} virtual void configure( const rclcpp_lifecycle::LifecycleNode::WeakPtr &, std::string, std::shared_ptr, std::shared_ptr, std::shared_ptr) {} virtual void cleanup() {} virtual void activate() {} virtual void deactivate() {} virtual bool smooth( nav_msgs::msg::Path & path, const rclcpp::Duration & max_time) { assert(path.poses.size() == 2); if (path.poses.front() == path.poses.back()) { throw nav2_core::PlannerException("Start and goal pose must differ"); } auto max_time_ms = max_time.to_chrono(); std::this_thread::sleep_for(std::min(max_time_ms, 100ms)); // place dummy pose in the middle of the path geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = (path.poses.front().pose.position.x + path.poses.back().pose.position.x) / 2; pose.pose.position.y = (path.poses.front().pose.position.y + path.poses.back().pose.position.y) / 2; pose.pose.orientation.w = 1.0; path.poses.push_back(pose); return max_time_ms > 100ms; } private: std::string command_; std::chrono::system_clock::time_point start_time_; }; // Mocked class loader void onPluginDeletion(nav2_core::Smoother * obj) { if (nullptr != obj) { delete (obj); } } template<> pluginlib::UniquePtr pluginlib::ClassLoader:: createUniqueInstance(const std::string & lookup_name) { if (lookup_name != "DummySmoother") { // original method body if (!isClassLoaded(lookup_name)) { loadLibraryForClass(lookup_name); } try { std::string class_type = getClassType(lookup_name); pluginlib::UniquePtr obj = lowlevel_class_loader_.createUniqueInstance(class_type); return obj; } catch (const class_loader::CreateClassException & ex) { throw pluginlib::CreateClassException(ex.what()); } } // mocked plugin creation return std::unique_ptr>( new DummySmoother(), onPluginDeletion); } class DummyCostmapSubscriber : public nav2_costmap_2d::CostmapSubscriber { public: DummyCostmapSubscriber( nav2_util::LifecycleNode::SharedPtr node, const std::string & topic_name) : CostmapSubscriber(node, topic_name) { auto costmap = std::make_shared(); costmap->metadata.size_x = 100; costmap->metadata.size_y = 100; costmap->metadata.resolution = 0.1; costmap->metadata.origin.position.x = -5.0; costmap->metadata.origin.position.y = -5.0; costmap->data.resize(costmap->metadata.size_x * costmap->metadata.size_y, 0); for (unsigned int i = 0; i < costmap->metadata.size_y; ++i) { for (unsigned int j = 20; j < 40; ++j) { costmap->data[i * costmap->metadata.size_x + j] = 254; } } setCostmap(costmap); } void setCostmap(nav2_msgs::msg::Costmap::SharedPtr msg) { costmap_msg_ = msg; costmap_received_ = true; } }; class DummyFootprintSubscriber : public nav2_costmap_2d::FootprintSubscriber { public: DummyFootprintSubscriber( nav2_util::LifecycleNode::SharedPtr node, const std::string & topic_name, tf2_ros::Buffer & tf_) : FootprintSubscriber(node, topic_name, tf_) { auto footprint = std::make_shared(); footprint->header.frame_id = "base_link"; // global frame = robot frame to avoid tf lookup footprint->header.stamp = node->get_clock()->now(); geometry_msgs::msg::Point32 point; point.x = -0.2f; point.y = -0.2f; footprint->polygon.points.push_back(point); point.y = 0.2f; footprint->polygon.points.push_back(point); point.x = 0.2f; point.y = 0.0f; footprint->polygon.points.push_back(point); setFootprint(footprint); } void setFootprint(geometry_msgs::msg::PolygonStamped::SharedPtr msg) { footprint_ = msg; footprint_received_ = true; } }; class DummySmootherServer : public nav2_smoother::SmootherServer { public: DummySmootherServer() { // Override defaults default_ids_.clear(); default_ids_.resize(1, "SmoothPath"); set_parameter(rclcpp::Parameter("smoother_plugins", default_ids_)); default_types_.clear(); default_types_.resize(1, "DummySmoother"); } nav2_util::CallbackReturn on_configure(const rclcpp_lifecycle::State & state) { auto result = SmootherServer::on_configure(state); if (result != nav2_util::CallbackReturn::SUCCESS) { return result; } // Create dummy subscribers and collision checker auto node = shared_from_this(); costmap_sub_ = std::make_shared( node, "costmap_topic"); footprint_sub_ = std::make_shared( node, "footprint_topic", *tf_); collision_checker_ = std::make_shared( *costmap_sub_, *footprint_sub_, node->get_name()); return result; } }; // Define a test class to hold the context for the tests class SmootherTest : public ::testing::Test { public: SmootherTest() {} ~SmootherTest() {} void SetUp() override { node_ = std::make_shared( "LifecycleSmootherTestNode", rclcpp::NodeOptions()); smoother_server_ = std::make_shared(); smoother_server_->set_parameter( rclcpp::Parameter( "smoother_plugins", rclcpp::ParameterValue(std::vector(1, "DummySmoothPath")))); smoother_server_->declare_parameter( "DummySmoothPath.plugin", rclcpp::ParameterValue(std::string("DummySmoother"))); smoother_server_->configure(); smoother_server_->activate(); client_ = rclcpp_action::create_client( node_->get_node_base_interface(), node_->get_node_graph_interface(), node_->get_node_logging_interface(), node_->get_node_waitables_interface(), "smooth_path"); std::cout << "Setup complete." << std::endl; } void TearDown() override { smoother_server_->deactivate(); smoother_server_->cleanup(); smoother_server_->shutdown(); smoother_server_.reset(); client_.reset(); node_.reset(); } bool sendGoal( std::string smoother_id, double x_start, double y_start, double x_goal, double y_goal, std::chrono::milliseconds max_time, bool check_for_collisions) { if (!client_->wait_for_action_server(4s)) { std::cout << "Server not up" << std::endl; return false; } geometry_msgs::msg::PoseStamped pose; pose.pose.orientation.w = 1.0; auto goal = SmoothAction::Goal(); goal.smoother_id = smoother_id; pose.pose.position.x = x_start; pose.pose.position.y = y_start; goal.path.poses.push_back(pose); pose.pose.position.x = x_goal; pose.pose.position.y = y_goal; goal.path.poses.push_back(pose); goal.check_for_collisions = check_for_collisions; goal.max_smoothing_duration = rclcpp::Duration(max_time); auto future_goal = client_->async_send_goal(goal); if (rclcpp::spin_until_future_complete(node_, future_goal) != rclcpp::FutureReturnCode::SUCCESS) { std::cout << "failed sending goal" << std::endl; // failed sending the goal return false; } goal_handle_ = future_goal.get(); if (!goal_handle_) { std::cout << "goal was rejected" << std::endl; // goal was rejected by the action server return false; } return true; } ClientGoalHandle::WrappedResult getResult() { std::cout << "Getting async result..." << std::endl; auto future_result = client_->async_get_result(goal_handle_); std::cout << "Waiting on future..." << std::endl; rclcpp::spin_until_future_complete(node_, future_result); std::cout << "future received!" << std::endl; return future_result.get(); } std::shared_ptr node_; std::shared_ptr smoother_server_; std::shared_ptr> client_; std::shared_ptr> goal_handle_; }; // Define the tests TEST_F(SmootherTest, testingSuccess) { ASSERT_TRUE(sendGoal("DummySmoothPath", 0.0, 0.0, 1.0, 0.0, 500ms, true)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED); EXPECT_EQ(result.result->path.poses.size(), (std::size_t)3); EXPECT_TRUE(result.result->was_completed); SUCCEED(); } TEST_F(SmootherTest, testingFailureOnInvalidSmootherId) { ASSERT_TRUE(sendGoal("InvalidSmoother", 0.0, 0.0, 1.0, 0.0, 500ms, true)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::ABORTED); SUCCEED(); } TEST_F(SmootherTest, testingSuccessOnEmptyPlugin) { ASSERT_TRUE(sendGoal("", 0.0, 0.0, 1.0, 0.0, 500ms, true)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED); SUCCEED(); } TEST_F(SmootherTest, testingIncomplete) { ASSERT_TRUE(sendGoal("DummySmoothPath", 0.0, 0.0, 1.0, 0.0, 50ms, true)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED); EXPECT_FALSE(result.result->was_completed); SUCCEED(); } TEST_F(SmootherTest, testingFailureOnException) { ASSERT_TRUE(sendGoal("DummySmoothPath", 0.0, 0.0, 0.0, 0.0, 500ms, true)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::ABORTED); SUCCEED(); } TEST_F(SmootherTest, testingFailureOnCollision) { ASSERT_TRUE(sendGoal("DummySmoothPath", -4.0, 0.0, 0.0, 0.0, 500ms, true)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::ABORTED); SUCCEED(); } TEST_F(SmootherTest, testingCollisionCheckDisabled) { ASSERT_TRUE(sendGoal("DummySmoothPath", -4.0, 0.0, 0.0, 0.0, 500ms, false)); auto result = getResult(); EXPECT_EQ(result.code, rclcpp_action::ResultCode::SUCCEEDED); SUCCEED(); } TEST(SmootherConfigTest, testingConfigureSuccessWithValidSmootherPlugin) { auto smoother_server = std::make_shared(); smoother_server->set_parameter( rclcpp::Parameter( "smoother_plugins", rclcpp::ParameterValue(std::vector(1, "DummySmoothPath")))); smoother_server->declare_parameter( "DummySmoothPath.plugin", rclcpp::ParameterValue(std::string("DummySmoother"))); auto state = smoother_server->configure(); EXPECT_EQ(state.id(), 2); // 1 on failure, 2 on success SUCCEED(); } TEST(SmootherConfigTest, testingConfigureFailureWithInvalidSmootherPlugin) { auto smoother_server = std::make_shared(); smoother_server->set_parameter( rclcpp::Parameter( "smoother_plugins", rclcpp::ParameterValue(std::vector(1, "DummySmoothPath")))); smoother_server->declare_parameter( "DummySmoothPath.plugin", rclcpp::ParameterValue(std::string("InvalidSmootherPlugin"))); auto state = smoother_server->configure(); EXPECT_EQ(state.id(), 1); // 1 on failure, 2 on success SUCCEED(); } TEST(SmootherConfigTest, testingConfigureSuccessWithDefaultPlugin) { auto smoother_server = std::make_shared(); auto state = smoother_server->configure(); EXPECT_EQ(state.id(), 2); // 1 on failure, 2 on success SUCCEED(); } int main(int argc, char ** argv) { ::testing::InitGoogleTest(&argc, argv); // initialize ROS rclcpp::init(argc, argv); bool all_successful = RUN_ALL_TESTS(); // shutdown ROS rclcpp::shutdown(); return all_successful; }