// Copyright (C) 2018, 2019 Kevin Hallenbeck, Joshua Whitley // All rights reserved. // // Software License Agreement (BSD License 2.0) // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions // are met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following // disclaimer in the documentation and/or other materials provided // with the distribution. // * Neither the name of {copyright_holder} nor the names of its // contributors may be used to endorse or promote products derived // from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE // COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. #include <gtest/gtest.h> #include <ros/ros.h> #include <sensor_msgs/PointCloud2.h> #include <sensor_msgs/LaserScan.h> #include <cstdlib> #include <algorithm> #include <vector> // Define our own PointCloud type for easy use typedef struct { float x; // x float y; // y float z; // z float i; // intensity uint16_t r; // ring } Point; typedef struct { std_msgs::Header header; std::vector<Point> points; } PointCloud; // Global variables ros::Publisher g_pub; ros::Subscriber g_sub; sensor_msgs::LaserScan g_scan; volatile bool g_scan_new = false; // Convert WallTime to Time static inline ros::Time rosTime(const ros::WallTime &stamp) { return ros::Time(stamp.sec, stamp.nsec); } // Subscriber receive callback void recv(const sensor_msgs::LaserScanConstPtr& msg) { g_scan = *msg; g_scan_new = true; } // Wait for incoming LaserScan message bool waitForScan(ros::WallDuration dur) { const ros::WallTime start = ros::WallTime::now(); while (!g_scan_new) { if ((ros::WallTime::now() - start) > dur) { return false; } ros::WallDuration(0.001).sleep(); ros::spinOnce(); } return true; } // Build and publish PointCloud2 messages of various structures void publishXYZIR1(const PointCloud &cloud) { g_scan_new = false; const uint32_t POINT_STEP = 32; sensor_msgs::PointCloud2 msg; msg.header.frame_id = cloud.header.frame_id; msg.header.stamp = cloud.header.stamp; msg.fields.resize(5); msg.fields[0].name = "x"; msg.fields[0].offset = 0; msg.fields[0].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[0].count = 1; msg.fields[1].name = "y"; msg.fields[1].offset = 4; msg.fields[1].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[1].count = 1; msg.fields[2].name = "z"; msg.fields[2].offset = 8; msg.fields[2].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[2].count = 1; msg.fields[3].name = "intensity"; msg.fields[3].offset = 16; msg.fields[3].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[3].count = 1; msg.fields[4].name = "ring"; msg.fields[4].offset = 20; msg.fields[4].datatype = sensor_msgs::PointField::UINT16; msg.fields[4].count = 1; msg.data.resize(std::max((size_t)1, cloud.points.size()) * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; msg.is_bigendian = false; msg.is_dense = true; uint8_t *ptr = msg.data.data(); for (size_t i = 0; i < cloud.points.size(); i++) { *(reinterpret_cast<float*>(ptr + 0)) = cloud.points[i].x; *(reinterpret_cast<float*>(ptr + 4)) = cloud.points[i].y; *(reinterpret_cast<float*>(ptr + 8)) = cloud.points[i].z; *(reinterpret_cast<float*>(ptr + 16)) = cloud.points[i].i; *(reinterpret_cast<uint16_t*>(ptr + 20)) = cloud.points[i].r; ptr += POINT_STEP; } g_pub.publish(msg); } void publishXYZIR2(const PointCloud &cloud) { g_scan_new = false; const uint32_t POINT_STEP = 19; sensor_msgs::PointCloud2 msg; msg.header.frame_id = cloud.header.frame_id; msg.header.stamp = cloud.header.stamp; msg.fields.resize(5); msg.fields[0].name = "z"; msg.fields[0].offset = 4; msg.fields[0].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[0].count = 1; msg.fields[1].name = "y"; msg.fields[1].offset = 8; msg.fields[1].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[1].count = 1; msg.fields[2].name = "x"; msg.fields[2].offset = 12; msg.fields[2].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[2].count = 1; msg.fields[3].name = "intensity"; msg.fields[3].offset = 0; msg.fields[3].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[3].count = 1; msg.fields[4].name = "ring"; msg.fields[4].offset = 16; msg.fields[4].datatype = sensor_msgs::PointField::UINT16; msg.fields[4].count = 1; msg.data.resize(std::max((size_t)1, cloud.points.size()) * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; msg.is_bigendian = false; msg.is_dense = true; uint8_t *ptr = msg.data.data(); for (size_t i = 0; i < cloud.points.size(); i++) { *(reinterpret_cast<float*>(ptr + 0)) = cloud.points[i].i; *(reinterpret_cast<float*>(ptr + 4)) = cloud.points[i].z; *(reinterpret_cast<float*>(ptr + 8)) = cloud.points[i].y; *(reinterpret_cast<float*>(ptr + 12)) = cloud.points[i].x; *(reinterpret_cast<uint16_t*>(ptr + 16)) = cloud.points[i].r; ptr += POINT_STEP; } g_pub.publish(msg); } void publishXYZR(const PointCloud &cloud) { g_scan_new = false; const uint32_t POINT_STEP = 15; sensor_msgs::PointCloud2 msg; msg.header.frame_id = cloud.header.frame_id; msg.header.stamp = cloud.header.stamp; msg.fields.resize(4); msg.fields[0].name = "x"; msg.fields[0].offset = 0; msg.fields[0].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[0].count = 1; msg.fields[1].name = "y"; msg.fields[1].offset = 4; msg.fields[1].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[1].count = 1; msg.fields[2].name = "z"; msg.fields[2].offset = 8; msg.fields[2].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[2].count = 1; msg.fields[3].name = "ring"; msg.fields[3].offset = 12; msg.fields[3].datatype = sensor_msgs::PointField::UINT16; msg.fields[3].count = 1; msg.data.resize(std::max((size_t)1, cloud.points.size()) * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; msg.is_bigendian = false; msg.is_dense = true; uint8_t *ptr = msg.data.data(); for (size_t i = 0; i < cloud.points.size(); i++) { *(reinterpret_cast<float*>(ptr + 0)) = cloud.points[i].x; *(reinterpret_cast<float*>(ptr + 4)) = cloud.points[i].y; *(reinterpret_cast<float*>(ptr + 8)) = cloud.points[i].z; *(reinterpret_cast<uint16_t*>(ptr + 12)) = cloud.points[i].r; ptr += POINT_STEP; } g_pub.publish(msg); } void publishR(const PointCloud &cloud) { g_scan_new = false; const uint32_t POINT_STEP = 2; sensor_msgs::PointCloud2 msg; msg.header.stamp = rosTime(ros::WallTime::now()); msg.fields.resize(1); msg.fields[0].name = "ring"; msg.fields[0].offset = 0; msg.fields[0].datatype = sensor_msgs::PointField::UINT16; msg.fields[0].count = 1; msg.data.resize(std::max((size_t)1, cloud.points.size()) * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; uint8_t *ptr = msg.data.data(); for (size_t i = 0; i < cloud.points.size(); i++) { *(reinterpret_cast<uint16_t*>(ptr + 0)) = cloud.points[i].r; ptr += POINT_STEP; } g_pub.publish(msg); } void publishXYZR32(const PointCloud &cloud) { g_scan_new = false; const uint32_t POINT_STEP = 16; sensor_msgs::PointCloud2 msg; msg.header.frame_id = cloud.header.frame_id; msg.header.stamp = cloud.header.stamp; msg.fields.resize(4); msg.fields[0].name = "x"; msg.fields[0].offset = 0; msg.fields[0].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[0].count = 1; msg.fields[1].name = "y"; msg.fields[1].offset = 4; msg.fields[1].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[1].count = 1; msg.fields[2].name = "z"; msg.fields[2].offset = 8; msg.fields[2].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[2].count = 1; msg.fields[3].name = "ring"; msg.fields[3].offset = 12; msg.fields[3].datatype = sensor_msgs::PointField::UINT32; msg.fields[3].count = 1; msg.data.resize(std::max((size_t)1, cloud.points.size()) * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; msg.is_bigendian = false; msg.is_dense = true; uint8_t *ptr = msg.data.data(); for (size_t i = 0; i < cloud.points.size(); i++) { *(reinterpret_cast<float*>(ptr + 0)) = cloud.points[i].x; *(reinterpret_cast<float*>(ptr + 4)) = cloud.points[i].y; *(reinterpret_cast<float*>(ptr + 8)) = cloud.points[i].z; *(reinterpret_cast<uint32_t*>(ptr + 12)) = cloud.points[i].r; ptr += POINT_STEP; } g_pub.publish(msg); } void publishXYZ(const PointCloud &cloud) { g_scan_new = false; const uint32_t POINT_STEP = 12; sensor_msgs::PointCloud2 msg; msg.header.stamp = rosTime(ros::WallTime::now()); msg.fields.resize(3); msg.fields[0].name = "x"; msg.fields[0].offset = 0; msg.fields[0].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[0].count = 1; msg.fields[1].name = "y"; msg.fields[1].offset = 4; msg.fields[1].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[1].count = 1; msg.fields[2].name = "z"; msg.fields[2].offset = 8; msg.fields[2].datatype = sensor_msgs::PointField::FLOAT32; msg.fields[2].count = 1; msg.data.resize(std::max((size_t)1, cloud.points.size()) * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; uint8_t *ptr = msg.data.data(); for (size_t i = 0; i < cloud.points.size(); i++) { *(reinterpret_cast<float*>(ptr + 0)) = cloud.points[i].x; *(reinterpret_cast<float*>(ptr + 4)) = cloud.points[i].y; *(reinterpret_cast<float*>(ptr + 8)) = cloud.points[i].z; ptr += POINT_STEP; } g_pub.publish(msg); } void publishNone() { g_scan_new = false; const uint32_t POINT_STEP = 16; sensor_msgs::PointCloud2 msg; msg.header.stamp = rosTime(ros::WallTime::now()); msg.data.resize(1 * POINT_STEP, 0x00); msg.point_step = POINT_STEP; msg.row_step = msg.data.size(); msg.height = 1; msg.width = msg.row_step / POINT_STEP; g_pub.publish(msg); } // Find the index of the point in the PointCloud with the shortest 2d distance to the point (x,y) static inline float SQUARE(float x) { return x * x; } size_t findClosestIndex(const PointCloud &cloud, uint16_t ring, float x, float y) { size_t index = SIZE_MAX; float delta = INFINITY; for (size_t i = 0; i < cloud.points.size(); i++) { if (cloud.points[i].r == ring) { float dist = SQUARE(x - cloud.points[i].x) + SQUARE(y - cloud.points[i].y); if (dist < delta) { delta = dist; index = i; } } } return index; } // Verify that all LaserScan header values are values are passed through, and other values are default void verifyScanEmpty(const PointCloud &cloud, bool intensity = true) { ASSERT_EQ(cloud.header.stamp, g_scan.header.stamp); EXPECT_EQ(cloud.header.frame_id, g_scan.header.frame_id); for (size_t i = 0; i < g_scan.ranges.size(); i++) { EXPECT_EQ(INFINITY, g_scan.ranges[i]); } if (!intensity) { EXPECT_EQ(0, g_scan.intensities.size()); } else { EXPECT_EQ(g_scan.ranges.size(), g_scan.intensities.size()); for (size_t i = 0; i < g_scan.intensities.size(); i++) { EXPECT_EQ(0.0, g_scan.intensities[i]); } } } // Verify that every PointCloud point made it to the LaserScan and other values are default void verifyScanSparse(const PointCloud &cloud, uint16_t ring, uint16_t ring_count, bool intensity = true) { ASSERT_EQ(cloud.header.stamp, g_scan.header.stamp); EXPECT_EQ(cloud.header.frame_id, g_scan.header.frame_id); EXPECT_EQ(intensity ? g_scan.ranges.size() : 0, g_scan.intensities.size()); size_t count = 0; for (size_t i = 0; i < g_scan.ranges.size(); i++) { double r = g_scan.ranges[i]; if (std::isfinite(r)) { float a = g_scan.angle_min + i * g_scan.angle_increment; float x = g_scan.ranges[i] * cosf(a); float y = g_scan.ranges[i] * sinf(a); float e = g_scan.ranges[i] * g_scan.angle_increment + static_cast<float>(1e-3); // allowable error size_t index = findClosestIndex(cloud, ring, x, y); if (index < cloud.points.size()) { count++; EXPECT_NEAR(cloud.points[index].x, x, e); EXPECT_NEAR(cloud.points[index].y, y, e); if (i < g_scan.intensities.size()) { EXPECT_EQ(cloud.points[index].i, g_scan.intensities[i]); } } else { EXPECT_TRUE(false); // LaserScan point not found in PointCloud } } else { EXPECT_EQ(INFINITY, r); } } if (ring_count > 0) { EXPECT_EQ(cloud.points.size() / ring_count, count); // Make sure that all points were converted to ranges } } // Verify that every LaserScan point is not default, and every point came from the PointCloud void verifyScanDense(const PointCloud &cloud, uint16_t ring, bool intensity = true) { ASSERT_EQ(cloud.header.stamp, g_scan.header.stamp); EXPECT_EQ(cloud.header.frame_id, g_scan.header.frame_id); EXPECT_EQ(intensity ? g_scan.ranges.size() : 0, g_scan.intensities.size()); for (size_t i = 0; i < g_scan.ranges.size(); i++) { double r = g_scan.ranges[i]; if (std::isfinite(r)) { float a = g_scan.angle_min + i * g_scan.angle_increment; float x = g_scan.ranges[i] * cosf(a); float y = g_scan.ranges[i] * sinf(a); float e = g_scan.ranges[i] * g_scan.angle_increment + static_cast<float>(1e-3); // allowable error size_t index = findClosestIndex(cloud, ring, x, y); if (index < cloud.points.size()) { EXPECT_NEAR(cloud.points[index].x, x, e); EXPECT_NEAR(cloud.points[index].y, y, e); // @TODO: Test for matching intensity } else { EXPECT_TRUE(false); // LaserScan point not found in PointCloud } } else { EXPECT_TRUE(false); // Dense PointCloud should populate every range in LaserScan } } } // Verify that no LaserScan is generated when the PointCloud2 message is missing required fields TEST(System, missing_fields) { // Make sure system is connected ASSERT_EQ(1, g_sub.getNumPublishers()); ASSERT_EQ(1, g_pub.getNumSubscribers()); // Create PointCloud with 16 rings PointCloud cloud; cloud.points.resize(1); cloud.points[0].x = 0.0; cloud.points[0].y = 0.0; cloud.points[0].z = 0.0; cloud.points[0].i = 0.0; cloud.points[0].r = 15; // Verify no LaserScan when PointCloud2 fields are empty publishNone(); EXPECT_FALSE(waitForScan(ros::WallDuration(0.5))); // Verify no LaserScan when PointCloud2 fields are missing 'ring' publishXYZ(cloud); EXPECT_FALSE(waitForScan(ros::WallDuration(0.5))); // Verify no LaserScan when PointCloud2 field 'ring' is the incorrect type publishXYZR32(cloud); EXPECT_FALSE(waitForScan(ros::WallDuration(0.5))); // Verify no LaserScan when PointCloud2 fields are missing 'x' and 'y' publishR(cloud); EXPECT_FALSE(waitForScan(ros::WallDuration(0.5))); // Verify that the node hasn't crashed by sending normal PointCloud2 fields cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR1(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); ASSERT_EQ(cloud.header.stamp, g_scan.header.stamp); } // Verify that non-point fields are passed through unmodified TEST(System, empty_data) { // Make sure system is connected ASSERT_EQ(1, g_sub.getNumPublishers()); ASSERT_EQ(1, g_pub.getNumSubscribers()); // Create PointCloud with 16 rings PointCloud cloud; cloud.header.frame_id = "abcdefghijklmnopqrstuvwxyz"; cloud.points.resize(1); cloud.points[0].x = 0.0; cloud.points[0].y = 0.0; cloud.points[0].z = 0.0; cloud.points[0].i = 0.0; cloud.points[0].r = 15; // Verify that all three PointCloud2 types create proper default values // PointXYZIR (expected format) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR1(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanEmpty(cloud, true); // PointXYZIR (unexpected format with intensity) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR2(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanEmpty(cloud, true); // PointXYZR (unexpected format without intensity) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZR(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanEmpty(cloud, false); } // Verify that every piece of a small amount of random data is passed through TEST(System, random_data_sparse) { // Make sure system is connected ASSERT_EQ(1, g_sub.getNumPublishers()); ASSERT_EQ(1, g_pub.getNumSubscribers()); // Create PointCloud with sparse random data PointCloud cloud; cloud.header.frame_id = "velodyne"; const size_t RANGE_COUNT = 100; const size_t RING_COUNT = 16; const double RANGE_MAX = 20.0; const double INTENSITY_MAX = 1.0; for (size_t i = 0; i < RANGE_COUNT; i++) { double angle_y = i * 1.99 * M_PI / RANGE_COUNT; // yaw for (size_t j = 0; j < RING_COUNT; j++) { double angle_p = j * 0.2 * M_PI / RING_COUNT - 0.1 * M_PI; // pitch double range = std::rand() * (RANGE_MAX / RAND_MAX); Point point; point.x = range * cos(angle_p) * cos(angle_y); point.y = range * cos(angle_p) * sin(angle_y); point.z = range * sin(angle_p); point.i = std::rand() * (INTENSITY_MAX / RAND_MAX); point.r = j; cloud.points.push_back(point); } } // Verify that all three PointCloud2 types are handled correctly // PointXYZIR (expected format) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR1(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanSparse(cloud, 8, RING_COUNT, true); // PointXYZIR (unexpected format with intensity) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR2(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanSparse(cloud, 8, RING_COUNT, true); // PointXYZR (unexpected format without intensity) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZR(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanSparse(cloud, 8, RING_COUNT, false); } // Verify that every LaserScan range is valid when given an extra large amount of random data TEST(System, random_data_dense) { // Make sure system is connected ASSERT_EQ(1, g_sub.getNumPublishers()); ASSERT_EQ(1, g_pub.getNumSubscribers()); // Create PointCloud with dense random data PointCloud cloud; cloud.header.frame_id = "velodyne"; const size_t RANGE_COUNT = 2500; const size_t RING_COUNT = 16; const double RANGE_MAX = 20.0; const double INTENSITY_MAX = 1.0; for (size_t i = 0; i < RANGE_COUNT; i++) { double angle_y = i * 2.0 * M_PI / RANGE_COUNT; // yaw for (size_t j = 0; j < RING_COUNT; j++) { double angle_p = j * 0.2 * M_PI / RING_COUNT - 0.1 * M_PI; // pitch double range = std::rand() * (RANGE_MAX / RAND_MAX); Point point; point.x = range * cos(angle_p) * cos(angle_y); point.y = range * cos(angle_p) * sin(angle_y); point.z = range * sin(angle_p); point.i = std::rand() * (INTENSITY_MAX / RAND_MAX); point.r = j; cloud.points.push_back(point); } } // Verify that all three PointCloud2 types are handled correctly // PointXYZIR (expected format) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR1(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanDense(cloud, 8, true); // PointXYZIR (unexpected format with intensity) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZIR2(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanDense(cloud, 8, true); // PointXYZR (unexpected format without intensity) cloud.header.stamp = rosTime(ros::WallTime::now()); publishXYZR(cloud); ASSERT_TRUE(waitForScan(ros::WallDuration(1.0))); verifyScanDense(cloud, 8, false); } int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); // Initialize ROS ros::init(argc, argv, "test_lazy_subscriber"); ros::NodeHandle nh; // Setup publisher and subscriber g_pub = nh.advertise<sensor_msgs::PointCloud2>("velodyne_points", 2); g_sub = nh.subscribe("scan", 2, recv); // Wait for other nodes to startup ros::WallDuration(1.0).sleep(); ros::spinOnce(); // Run all the tests that were declared with TEST() return RUN_ALL_TESTS(); }