|
| 1 | +# Testing Guide for elevation_mapping_cupy |
| 2 | + |
| 3 | +## Running Tests Locally |
| 4 | + |
| 5 | +### Prerequisites |
| 6 | + |
| 7 | +```bash |
| 8 | +# Source ROS2 |
| 9 | +source /opt/ros/jazzy/setup.bash |
| 10 | + |
| 11 | +# Build the package |
| 12 | +colcon build --packages-select elevation_mapping_cupy |
| 13 | + |
| 14 | +# Source the workspace |
| 15 | +source install/setup.bash |
| 16 | +``` |
| 17 | + |
| 18 | +### Run All Tests via colcon |
| 19 | + |
| 20 | +```bash |
| 21 | +colcon test --packages-select elevation_mapping_cupy --event-handlers console_direct+ |
| 22 | +``` |
| 23 | + |
| 24 | +### Run Unit Tests Only (pytest) |
| 25 | + |
| 26 | +These are the primary regression tests for the axis-swap bug. They don't require ROS nodes to be running. |
| 27 | + |
| 28 | +```bash |
| 29 | +# Run all unit tests |
| 30 | +cd elevation_mapping_cupy/elevation_mapping_cupy/tests/ |
| 31 | +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest -v |
| 32 | + |
| 33 | +# Run specific test file |
| 34 | +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest test_map_shifting.py -v |
| 35 | + |
| 36 | +# Run specific test |
| 37 | +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest test_map_shifting.py::TestShiftMapXY::test_shift_x_only_affects_columns -v |
| 38 | +``` |
| 39 | + |
| 40 | +### Run Integration Tests Only (launch_testing) |
| 41 | + |
| 42 | +These test the full TF → GridMap pipeline with actual ROS nodes. |
| 43 | + |
| 44 | +```bash |
| 45 | +# Stop daemon first to avoid DDS issues |
| 46 | +ros2 daemon stop |
| 47 | + |
| 48 | +# Run with DDS fixes |
| 49 | +FASTDDS_BUILTIN_TRANSPORTS=UDPv4 python3 -m launch_testing.launch_test \ |
| 50 | + src/elevation_mapping_cupy/elevation_mapping_cupy/test/test_tf_gridmap_integration.py |
| 51 | +``` |
| 52 | + |
| 53 | +### Test Summary |
| 54 | + |
| 55 | +| Test File | Type | What it tests | |
| 56 | +|-----------|------|---------------| |
| 57 | +| `test_map_shifting.py` | Unit (pytest) | Axis-swap bug regression - `shift_map_xy()` function | |
| 58 | +| `test_map_services.py` | Unit (pytest) | Map service handlers | |
| 59 | +| `test_tf_gridmap_integration.py` | Integration (launch_testing) | Full TF → GridMap pipeline | |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +# ROS2 Integration Testing: DDS Discovery Fixes |
| 64 | + |
| 65 | +This document captures lessons learned from fixing DDS discovery issues in `launch_testing` integration tests. |
| 66 | + |
| 67 | +## Problem |
| 68 | + |
| 69 | +DDS discovery between the test fixture process and launched nodes fails intermittently in `launch_testing`. Symptoms: |
| 70 | +- Test fixture cannot subscribe to topics published by launched nodes |
| 71 | +- `wait_for_gridmap()` times out even though node is publishing |
| 72 | +- Tests pass locally sometimes but fail in CI |
| 73 | + |
| 74 | +## Root Causes & Fixes |
| 75 | + |
| 76 | +### 1. ROS2 Daemon RMW Mismatch |
| 77 | + |
| 78 | +**Cause**: The ROS2 daemon may be running with a different RMW implementation than the tests. This causes discovery timeouts. |
| 79 | + |
| 80 | +**Fix**: Stop the daemon before tests run. |
| 81 | + |
| 82 | +```python |
| 83 | +# In generate_test_description() or setUpClass() |
| 84 | +import subprocess |
| 85 | +subprocess.run(['ros2', 'daemon', 'stop'], capture_output=True) |
| 86 | +``` |
| 87 | + |
| 88 | +**Source**: [ros2/system_tests#460](https://github.com/ros2/system_tests/pull/460) |
| 89 | + |
| 90 | +### 2. FastDDS Shared Memory Transport Issues |
| 91 | + |
| 92 | +**Cause**: FastDDS uses shared memory (SHM) by default for same-machine communication. This can fail in certain environments (Docker, VMs, some Linux configurations). |
| 93 | + |
| 94 | +**Fix**: Force UDPv4 transport instead of shared memory. |
| 95 | + |
| 96 | +```python |
| 97 | +# In Python |
| 98 | +import os |
| 99 | +os.environ['FASTDDS_BUILTIN_TRANSPORTS'] = 'UDPv4' |
| 100 | +``` |
| 101 | + |
| 102 | +```cmake |
| 103 | +# In CMakeLists.txt |
| 104 | +add_launch_test(test/my_test.py |
| 105 | + ENV FASTDDS_BUILTIN_TRANSPORTS=UDPv4 |
| 106 | +) |
| 107 | +``` |
| 108 | + |
| 109 | +**Alternative**: Switch to CycloneDDS: |
| 110 | +```bash |
| 111 | +export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp |
| 112 | +``` |
| 113 | + |
| 114 | +**Source**: [ROS Answers: DDS discovery not working on same machine](https://answers.ros.org/question/407025/) |
| 115 | + |
| 116 | +### 3. Missing Domain ID Isolation |
| 117 | + |
| 118 | +**Cause**: Using `add_launch_test` without isolation can cause cross-talk between parallel tests. |
| 119 | + |
| 120 | +**Fix**: Use `add_ros_isolated_launch_test` for unique ROS_DOMAIN_ID per test. |
| 121 | + |
| 122 | +```cmake |
| 123 | +# In CMakeLists.txt |
| 124 | +find_package(ament_cmake_ros REQUIRED) |
| 125 | +
|
| 126 | +function(add_ros_isolated_launch_test path) |
| 127 | + set(RUNNER "${ament_cmake_ros_DIR}/run_test_isolated.py") |
| 128 | + add_launch_test("${path}" RUNNER "${RUNNER}" ${ARGN}) |
| 129 | +endfunction() |
| 130 | +
|
| 131 | +add_ros_isolated_launch_test(test/my_integration_test.py |
| 132 | + TIMEOUT 180 |
| 133 | +) |
| 134 | +``` |
| 135 | + |
| 136 | +**Source**: [ROS2 Integration Testing Docs](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Testing/Integration.html) |
| 137 | + |
| 138 | +### 4. QoS Profile Mismatch |
| 139 | + |
| 140 | +**Cause**: Incompatible QoS profiles between publisher and subscriber silently prevent message delivery. |
| 141 | + |
| 142 | +**Debug**: |
| 143 | +```bash |
| 144 | +ros2 topic info /my_topic -v # Shows QoS of all publishers/subscribers |
| 145 | +``` |
| 146 | + |
| 147 | +**Fix**: Ensure QoS compatibility. Common issues: |
| 148 | +- `RELIABLE` subscriber cannot receive from `BEST_EFFORT` publisher |
| 149 | +- `TRANSIENT_LOCAL` durability mismatch |
| 150 | + |
| 151 | +**Source**: [ROS2 QoS Documentation](https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Quality-of-Service-Settings.html) |
| 152 | + |
| 153 | +## Complete CMakeLists.txt Example |
| 154 | + |
| 155 | +```cmake |
| 156 | +if(BUILD_TESTING) |
| 157 | + find_package(ament_cmake_pytest REQUIRED) |
| 158 | + find_package(ament_cmake_ros REQUIRED) |
| 159 | + find_package(launch_testing_ament_cmake REQUIRED) |
| 160 | +
|
| 161 | + # Unit tests (no ROS dependencies) |
| 162 | + ament_add_pytest_test(test_my_module |
| 163 | + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_my_module.py |
| 164 | + TIMEOUT 120 |
| 165 | + ENV PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 # Avoid launch_testing import issues |
| 166 | + ) |
| 167 | +
|
| 168 | + # Define isolated launch test function |
| 169 | + function(add_ros_isolated_launch_test path) |
| 170 | + set(RUNNER "${ament_cmake_ros_DIR}/run_test_isolated.py") |
| 171 | + add_launch_test("${path}" RUNNER "${RUNNER}" ${ARGN}) |
| 172 | + endfunction() |
| 173 | +
|
| 174 | + # Integration test with DDS fixes |
| 175 | + add_ros_isolated_launch_test(test/test_integration.py |
| 176 | + TIMEOUT 180 |
| 177 | + ENV FASTDDS_BUILTIN_TRANSPORTS=UDPv4 |
| 178 | + ) |
| 179 | +endif() |
| 180 | +``` |
| 181 | + |
| 182 | +## Complete Test File Pattern |
| 183 | + |
| 184 | +```python |
| 185 | +import os |
| 186 | +import subprocess |
| 187 | +import unittest |
| 188 | + |
| 189 | +import rclpy |
| 190 | +from rclpy.node import Node |
| 191 | +from rclpy.executors import SingleThreadedExecutor |
| 192 | + |
| 193 | +import launch |
| 194 | +import launch_ros |
| 195 | +import launch_testing |
| 196 | + |
| 197 | +def generate_test_description(): |
| 198 | + # Stop daemon to avoid RMW mismatch |
| 199 | + subprocess.run(['ros2', 'daemon', 'stop'], capture_output=True) |
| 200 | + |
| 201 | + # Force UDPv4 transport |
| 202 | + os.environ['FASTDDS_BUILTIN_TRANSPORTS'] = 'UDPv4' |
| 203 | + |
| 204 | + node_under_test = launch_ros.actions.Node( |
| 205 | + package='my_package', |
| 206 | + executable='my_node', |
| 207 | + name='my_node', |
| 208 | + ) |
| 209 | + |
| 210 | + return ( |
| 211 | + launch.LaunchDescription([ |
| 212 | + node_under_test, |
| 213 | + launch_testing.actions.ReadyToTest(), |
| 214 | + ]), |
| 215 | + {'node_under_test': node_under_test} |
| 216 | + ) |
| 217 | + |
| 218 | +class TestIntegration(unittest.TestCase): |
| 219 | + @classmethod |
| 220 | + def setUpClass(cls): |
| 221 | + # Also stop daemon here in case generate_test_description ran in different process |
| 222 | + subprocess.run(['ros2', 'daemon', 'stop'], capture_output=True) |
| 223 | + |
| 224 | + try: |
| 225 | + rclpy.init() |
| 226 | + except RuntimeError: |
| 227 | + pass # Already initialized |
| 228 | + |
| 229 | + cls.node = Node('test_node') |
| 230 | + cls.executor = SingleThreadedExecutor() |
| 231 | + cls.executor.add_node(cls.node) |
| 232 | + |
| 233 | + @classmethod |
| 234 | + def tearDownClass(cls): |
| 235 | + cls.executor.shutdown() |
| 236 | + cls.node.destroy_node() |
| 237 | + |
| 238 | + def test_something(self): |
| 239 | + # Your test here |
| 240 | + pass |
| 241 | +``` |
| 242 | + |
| 243 | +## Debugging Tips |
| 244 | + |
| 245 | +1. **Check if daemon is running**: |
| 246 | + ```bash |
| 247 | + ros2 daemon status |
| 248 | + ``` |
| 249 | + |
| 250 | +2. **Check RMW implementation**: |
| 251 | + ```bash |
| 252 | + echo $RMW_IMPLEMENTATION |
| 253 | + ros2 doctor --report | grep middleware |
| 254 | + ``` |
| 255 | + |
| 256 | +3. **List all topics with QoS**: |
| 257 | + ```bash |
| 258 | + ros2 topic list -v |
| 259 | + ros2 topic info /my_topic -v |
| 260 | + ``` |
| 261 | + |
| 262 | +4. **Test DDS discovery manually**: |
| 263 | + ```bash |
| 264 | + # Terminal 1 |
| 265 | + ros2 topic pub /test std_msgs/String "data: hello" |
| 266 | + |
| 267 | + # Terminal 2 |
| 268 | + ros2 topic echo /test |
| 269 | + ``` |
| 270 | + |
| 271 | +5. **Force different DDS**: |
| 272 | + ```bash |
| 273 | + RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ros2 topic list |
| 274 | + ``` |
| 275 | + |
| 276 | +## References |
| 277 | + |
| 278 | +- [ROS2 Integration Testing Tutorial](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Testing/Integration.html) |
| 279 | +- [launch_testing GitHub](https://github.com/ros2/launch/tree/rolling/launch_testing) |
| 280 | +- [FastDDS Builtin Transports](https://fast-dds.docs.eprosima.com/en/latest/fastdds/transport/transport.html) |
| 281 | +- [ROS2 QoS Settings](https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Quality-of-Service-Settings.html) |
0 commit comments