/********************************************************************* * * Software License Agreement (BSD License) * * move_base2 — test đường vào cảm biến. * * Test này dựng `LayeredCostmap` THẬT và cắm vào đó các layer gián điệp. Lý do không dùng costmap * giả: thứ cần khoá lại ở đây chính là ba contract ẩn của `robot_costmap_2d`, và chúng chỉ tồn tại * trong lớp thật — * 1. `Layer::dataCallBack` xoá kiểu về `void*` + `std::type_info`, sai kiểu KHÔNG gây lỗi biên * dịch mà rơi im lặng; * 2. tham số `name` là khoá topic mà layer so lại, không phải nhãn tự do; * 3. bộ lọc chọn layer quyết định layer nào thấy dữ liệu. * Một costmap giả sẽ mô phỏng lại các contract đó theo cách tôi *nghĩ* chúng hoạt động — đúng loại * test không phát hiện được gì. * * Author: DuongTD *********************************************************************/ #include #include #include #include #include #include #include #include #include #include #include "spy_layer.h" using move_base2::SensorGateway; using move_base2::SensorGatewayConfig; using move_base2::testing::attachSpy; using move_base2::testing::SpyPtr; using robot_costmap_2d::LayerType; namespace { /** * @class Bench * @brief Một cặp costmap thật + cổng cảm biến đã gắn. */ class Bench { public: Bench() : global_("map", false, true) , local_("odom", true, false) { std::string error; EXPECT_TRUE(gateway_.configure(SensorGatewayConfig(), error)) << error; } /// @brief Cắm một layer gián điệp vào costmap global và trả con trỏ để kiểm tra sau. SpyPtr addGlobal(LayerType type, const std::string& name, bool enabled = true, bool explode = false) { return attachSpy(global_, type, name, enabled, explode); } SpyPtr addLocal(LayerType type, const std::string& name, bool enabled = true, bool explode = false) { return attachSpy(local_, type, name, enabled, explode); } void attach() { gateway_.attach(&global_, &local_); } SensorGateway& gateway() { return gateway_; } private: robot_costmap_2d::LayeredCostmap global_; robot_costmap_2d::LayeredCostmap local_; SensorGateway gateway_; }; robot_sensor_msgs::LaserScan makeScan(std::size_t rays = 40, float range = 1.0F) { robot_sensor_msgs::LaserScan scan; scan.header.frame_id = "laser"; scan.angle_min = -1.5F; // [rad] scan.angle_max = 1.5F; // [rad] scan.angle_increment = 3.0F / static_cast(rays); // [rad] scan.range_min = 0.05F; // [m] scan.range_max = 10.0F; // [m] scan.ranges.assign(rays, range); return scan; } robot_sensor_msgs::DepthCameraData::ConstPtr makeDepth() { robot_sensor_msgs::DepthCameraData::Ptr data = boost::make_shared(); data->header.frame_id = "camera_optical"; return data; } } // namespace // ================================================================================================ // Contract ẩn #1 — kiểu phải tới nơi ĐÚNG NHƯ khi gửi đi // ================================================================================================ TEST(SensorGateway, LaserScanArrivesWithLaserScanType) { Bench bench; SpyPtr voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); ASSERT_EQ(voxel->count(), 1U); EXPECT_TRUE(*voxel->records()[0].type == typeid(robot_sensor_msgs::LaserScan)); } TEST(SensorGateway, DepthCameraDataArrivesAsConstPtrNotAsValue) { // Đây là test đắt nhất của file. `ObstacleLayer::handleImpl` so // `typeid(DepthCameraData::ConstPtr)`; gửi đi dạng giá trị sẽ khớp `typeid(DepthCameraData)` và // rơi qua MỌI nhánh if mà không có lỗi biên dịch, không có log — depth camera lặng lẽ ngừng hoạt // động. Không có cách nào bắt được lỗi đó ngoài việc kiểm đúng type_info tới nơi. Bench bench; SpyPtr voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushDepthCameraData("/camera/depth/data", makeDepth()); ASSERT_EQ(voxel->count(), 1U); EXPECT_TRUE(*voxel->records()[0].type == typeid(robot_sensor_msgs::DepthCameraData::ConstPtr)); EXPECT_FALSE(*voxel->records()[0].type == typeid(robot_sensor_msgs::DepthCameraData)); } TEST(SensorGateway, StaticMapArrivesWithOccupancyGridType) { Bench bench; SpyPtr layer = bench.addGlobal(LayerType::STATIC_LAYER, "navigation_map"); bench.attach(); bench.gateway().pushStaticMap("/map", robot_nav_msgs::OccupancyGrid()); ASSERT_EQ(layer->count(), 1U); EXPECT_TRUE(*layer->records()[0].type == typeid(robot_nav_msgs::OccupancyGrid)); } TEST(SensorGateway, PointCloudAndPointCloud2AreDistinctTypes) { Bench bench; SpyPtr voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushPointCloud("/pc", robot_sensor_msgs::PointCloud()); bench.gateway().pushPointCloud2("/pc2", robot_sensor_msgs::PointCloud2()); ASSERT_EQ(voxel->count(), 2U); EXPECT_TRUE(*voxel->records()[0].type == typeid(robot_sensor_msgs::PointCloud)); EXPECT_TRUE(*voxel->records()[1].type == typeid(robot_sensor_msgs::PointCloud2)); } // ================================================================================================ // Contract ẩn #2 — `name` là khoá topic, phải tới nguyên văn // ================================================================================================ TEST(SensorGateway, TopicNameReachesTheLayerVerbatim) { // Layer so chuỗi này với `map_topic` / `observation_sources[i].topic` trong YAML. Sửa nó dù chỉ // một ký tự — kể cả thêm/bớt dấu '/' — là mất hẳn một cảm biến, không cảnh báo. Bench bench; SpyPtr voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); bench.gateway().pushDepthCameraData("/camera_right/depth/data", makeDepth()); ASSERT_EQ(voxel->count(), 2U); EXPECT_EQ(voxel->records()[0].topic, "/b_scan"); EXPECT_EQ(voxel->records()[1].topic, "/camera_right/depth/data"); } // ================================================================================================ // Contract ẩn #3 — bộ lọc chọn layer (C5) // ================================================================================================ TEST(SensorGateway, StaticMapGoesOnlyToStaticLayers) { Bench bench; SpyPtr static_layer = bench.addGlobal(LayerType::STATIC_LAYER, "navigation_map"); SpyPtr voxel = bench.addGlobal(LayerType::VOXEL_LAYER, "obstacles"); SpyPtr inflation = bench.addGlobal(LayerType::INFLATION_LAYER, "inflation"); bench.attach(); bench.gateway().pushStaticMap("/map", robot_nav_msgs::OccupancyGrid()); EXPECT_EQ(static_layer->count(), 1U); EXPECT_EQ(voxel->count(), 0U); EXPECT_EQ(inflation->count(), 0U); } TEST(SensorGateway, ObstacleDataGoesOnlyToVoxelLayers) { Bench bench; SpyPtr static_layer = bench.addLocal(LayerType::STATIC_LAYER, "navigation_map"); SpyPtr voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); SpyPtr inflation = bench.addLocal(LayerType::INFLATION_LAYER, "inflation"); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); EXPECT_EQ(voxel->count(), 1U); EXPECT_EQ(static_layer->count(), 0U); EXPECT_EQ(inflation->count(), 0U); } TEST(SensorGateway, LayerNamedAfterATopicDoesNotReceiveTheSample) { // Hồi quy cho C5. Bản `move_base` cũ lọc bằng // getType() == layer_type || getName() == name // Vế thứ hai là bẫy: đặt tên một layer trùng tên topic thì nó nhận dữ liệu nó không hiểu. Với // InflationLayer — có handleImpl chỉ biết log error — hậu quả là spam log ở đúng tần số cảm biến. Bench bench; SpyPtr trap = bench.addLocal(LayerType::INFLATION_LAYER, "/b_scan"); SpyPtr voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); EXPECT_EQ(trap->count(), 0U) << "a layer matching the topic NAME but with the wrong TYPE still " "received data"; EXPECT_EQ(voxel->count(), 1U); } TEST(SensorGateway, SampleReachesBothGlobalAndLocalCostmaps) { Bench bench; SpyPtr global_voxel = bench.addGlobal(LayerType::VOXEL_LAYER, "obstacles"); SpyPtr local_voxel = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); EXPECT_EQ(global_voxel->count(), 1U); EXPECT_EQ(local_voxel->count(), 1U); EXPECT_EQ(bench.gateway().stats().delivered, 2U); } // ================================================================================================ // Layer tắt (C8) và các nhánh bỏ mẫu — phải ĐẾM được, không im lặng // ================================================================================================ TEST(SensorGateway, DisabledLayerIsSkippedAndCounted) { Bench bench; SpyPtr disabled = bench.addGlobal(LayerType::VOXEL_LAYER, "obstacles", /*enabled=*/false); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); EXPECT_EQ(disabled->count(), 0U); EXPECT_EQ(bench.gateway().stats().skipped_disabled, 1U); EXPECT_EQ(bench.gateway().stats().delivered, 0U); } TEST(SensorGateway, SamplesBeforeAnyCostmapIsAttachedAreCounted) { // Bản cũ mở đầu bằng `if (!costmap) return;` không log. Mọi mẫu tới trước khi costmap tồn tại // biến mất không dấu vết, và đó là trạng thái BÌNH THƯỜNG lúc khởi động. SensorGateway gateway; std::string error; ASSERT_TRUE(gateway.configure(SensorGatewayConfig(), error)) << error; ASSERT_FALSE(gateway.attached()); gateway.pushStaticMap("/map", robot_nav_msgs::OccupancyGrid()); gateway.pushLaserScan("/b_scan", makeScan()); gateway.pushDepthCameraData("/camera/depth/data", makeDepth()); EXPECT_EQ(gateway.stats().dropped_no_costmap, 3U); EXPECT_EQ(gateway.stats().delivered, 0U); } TEST(SensorGateway, NullDepthPointerIsIgnoredWithoutCountingAsADrop) { Bench bench; bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushDepthCameraData("/camera/depth/data", robot_sensor_msgs::DepthCameraData::ConstPtr()); EXPECT_EQ(bench.gateway().stats().delivered, 0U); EXPECT_EQ(bench.gateway().stats().dropped_no_costmap, 0U); } TEST(SensorGateway, ExceptionFromOneLayerDoesNotStarveTheNextOnes) { // Bản cũ bọc try/catch quanh CẢ vòng lặp rồi `return`, nên một layer ném exception làm mọi layer // đứng sau nó mất luôn mẫu đó — và với một layer hỏng cố định thì mất vĩnh viễn. Bench bench; SpyPtr exploding = bench.addLocal(LayerType::VOXEL_LAYER, "broken", true, /*explode=*/true); SpyPtr healthy = bench.addLocal(LayerType::VOXEL_LAYER, "obstacles"); bench.attach(); bench.gateway().pushLaserScan("/b_scan", makeScan()); EXPECT_EQ(exploding->count(), 0U); EXPECT_EQ(healthy->count(), 1U) << "a healthy layer was skipped because the layer before it " "threw an exception"; EXPECT_EQ(bench.gateway().stats().layer_exceptions, 1U); EXPECT_EQ(bench.gateway().stats().delivered, 1U); } // ================================================================================================ // Lọc laser (C7) // ================================================================================================ TEST(SensorGatewayConfigTest, LaserFilterIsOffByDefaultAndLeavesTheScanUntouched) { SensorGateway gateway; std::string error; ASSERT_TRUE(gateway.configure(SensorGatewayConfig(), error)) << error; const robot_sensor_msgs::LaserScan scan = makeScan(); const robot_sensor_msgs::LaserScan prepared = gateway.prepareLaserScan(scan); EXPECT_EQ(prepared.ranges, scan.ranges); EXPECT_EQ(prepared.angle_increment, scan.angle_increment); } TEST(SensorGatewayConfigTest, EnabledLaserFilterKeepsScanStructure) { // Bộ lọc giữ nguyên cấu trúc scan (outlier thành NaN) — chỉ số tia phải khớp một-một với góc, nếu // không thì mọi phép chiếu tia sang điểm sau đó đều lệch. SensorGatewayConfig config; config.laser_sor_enabled = true; config.laser_sor_mean_k = 5; config.laser_sor_stddev_mul = 1.0; SensorGateway gateway; std::string error; ASSERT_TRUE(gateway.configure(config, error)) << error; const robot_sensor_msgs::LaserScan scan = makeScan(); const robot_sensor_msgs::LaserScan prepared = gateway.prepareLaserScan(scan); EXPECT_EQ(prepared.ranges.size(), scan.ranges.size()); EXPECT_EQ(prepared.angle_increment, scan.angle_increment); EXPECT_EQ(prepared.header.frame_id, scan.header.frame_id); } TEST(SensorGatewayConfigTest, RejectsOutOfRangeFilterParametersOnlyWhenFilterIsOn) { std::string error; SensorGatewayConfig off; off.laser_sor_enabled = false; off.laser_sor_mean_k = 0; // vô nghĩa, nhưng tính năng đang tắt off.laser_sor_stddev_mul = -1.0; // vô nghĩa, nhưng tính năng đang tắt EXPECT_TRUE(off.validate(error)) << error; SensorGatewayConfig bad_k; bad_k.laser_sor_enabled = true; bad_k.laser_sor_mean_k = 1; EXPECT_FALSE(bad_k.validate(error)); SensorGatewayConfig bad_mul; bad_mul.laser_sor_enabled = true; bad_mul.laser_sor_stddev_mul = 0.0; EXPECT_FALSE(bad_mul.validate(error)); } TEST(SensorGatewayConfigTest, ConfigureFailsAndReportsWhyOnInvalidParameters) { SensorGatewayConfig config; config.laser_sor_enabled = true; config.laser_sor_mean_k = 0; SensorGateway gateway; std::string error; EXPECT_FALSE(gateway.configure(config, error)); EXPECT_FALSE(error.empty()); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }