Skip to content

Commit f174f70

Browse files
committed
added tests
1 parent daffac3 commit f174f70

File tree

8 files changed

+1063
-27
lines changed

8 files changed

+1063
-27
lines changed

deep_object_detection/CMakeLists.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ install(DIRECTORY config launch
104104

105105
if(BUILD_TESTING)
106106
find_package(deep_test REQUIRED)
107+
find_package(launch_testing_ament_cmake REQUIRED)
108+
109+
# Unit tests
107110
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/test/test_postprocessors.cpp)
108111
add_deep_test(test_postprocessors test/test_postprocessors.cpp
109112
LIBRARIES
@@ -125,6 +128,29 @@ if(BUILD_TESTING)
125128
${rcl_interfaces_TARGETS}
126129
)
127130
endif()
131+
132+
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/test/test_deep_object_detection_node.cpp)
133+
add_deep_test(test_deep_object_detection_node test/test_deep_object_detection_node.cpp
134+
LIBRARIES
135+
deep_object_detection_lib
136+
${rclcpp_lifecycle_TARGETS}
137+
${vision_msgs_TARGETS}
138+
${deep_msgs_TARGETS}
139+
)
140+
# Set explicit timeout for unit test
141+
set_tests_properties(test_deep_object_detection_node PROPERTIES TIMEOUT 10)
142+
endif()
143+
144+
# Launch tests disabled by default
145+
# Set ENABLE_LAUNCH_TESTS=1 to enable them
146+
if(DEFINED ENV{ENABLE_LAUNCH_TESTS} AND "$ENV{ENABLE_LAUNCH_TESTS}" STREQUAL "1")
147+
message(STATUS "Launch tests enabled (CPU/GPU/TensorRT)")
148+
add_deep_launch_test(test/launch_tests/test_deep_object_detection_cpu_backend.py TIMEOUT 60)
149+
add_deep_launch_test(test/launch_tests/test_deep_object_detection_gpu_backend.py TIMEOUT 60)
150+
add_deep_launch_test(test/launch_tests/test_deep_object_detection_tensorrt_backend.py TIMEOUT 60)
151+
else()
152+
message(STATUS "Launch tests disabled by default (set ENABLE_LAUNCH_TESTS=1 to enable)")
153+
endif()
128154
endif()
129155

130156
ament_package()

deep_object_detection/README.md

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -265,32 +265,69 @@ Topic names can be configured either via parameters in the config file or via re
265265
- from: "$(var config_file)"
266266
```
267267

268-
## Batching Behavior
268+
## Limitations
269269

270-
The node supports flexible batching with three parameters:
270+
1. **MultiImage input only**: The node only supports MultiImage messages. Individual camera topics are not supported.
271+
2. **Compressed images only**: Only compressed images (sensor_msgs/CompressedImage) are supported. Raw images are not supported.
272+
3. **No dynamic reconfiguration**: Parameters cannot be changed at runtime. Node must be reconfigured to change parameters.
271273

272-
- **`min_batch_size`**: Minimum number of images required before processing (default: 1)
273-
- **`max_batch_size`**: Maximum number of images per batch (default: 3)
274-
- **`max_batch_latency_ms`**: Maximum wait time in milliseconds before processing even if `min_batch_size` is not met (default: 0 = wait indefinitely)
274+
## Testing
275275

276-
**Use Cases:**
277-
- **Single camera**: `min_batch_size=1, max_batch_size=1` - process each image immediately
278-
- **Multi-camera synchronized**: `min_batch_size=N, max_batch_size=N` where N = number of cameras
279-
- **Best-effort batching**: `min_batch_size=1, max_batch_size=N` with optional `max_batch_latency_ms` timeout
276+
The package includes both unit tests (C++) and launch tests (Python) for comprehensive testing.
280277

281-
## Limitations
278+
### Unit Tests (C++)
279+
280+
Fast unit tests using the `deep_test` framework that verify node construction, parameter handling, and lifecycle state management without requiring model files or GPU access.
281+
282+
**Run unit tests:**
283+
284+
```bash
285+
# Build with tests enabled
286+
colcon build --packages-select deep_object_detection --cmake-args -DBUILD_TESTING=ON
287+
288+
# Run tests
289+
source install/setup.bash
290+
colcon test --packages-select deep_object_detection
291+
292+
# View results
293+
colcon test-result --verbose
294+
```
295+
296+
**Run specific test:**
297+
298+
```bash
299+
./build/deep_object_detection/test_deep_object_detection_node "[node][construction]"
300+
```
301+
302+
### Launch Tests (Python)
303+
304+
Integration tests that launch the full node with different backends (CPU, CUDA, TensorRT) and verify end-to-end functionality including model loading, inference, and detection output.
305+
306+
**Note:** Launch tests are **disabled by default** to keep test runs fast. They require model loading which is slow (~30-60 seconds per test).
307+
308+
**Run launch tests (opt-in, requires model file):**
309+
310+
```bash
311+
# Enable launch tests explicitly
312+
export ENABLE_LAUNCH_TESTS=1
313+
# Build and run
314+
colcon build --packages-select deep_object_detection --cmake-args -DBUILD_TESTING=ON
315+
source install/setup.bash
316+
colcon test --packages-select deep_object_detection
317+
318+
# View results
319+
colcon test-result --verbose
320+
```
321+
322+
**Available launch tests:**
323+
- `test_deep_object_detection_cpu_backend.py` - CPU backend test
324+
- `test_deep_object_detection_gpu_backend.py` - CUDA backend test (requires GPU)
325+
- `test_deep_object_detection_tensorrt_backend.py` - TensorRT backend test (requires GPU + TensorRT)
326+
327+
Launch tests are disabled by default and must be explicitly enabled with `ENABLE_LAUNCH_TESTS=1`.
328+
329+
**Test Requirements:**
330+
- Model file: `/workspaces/deep_ros/yolov8m.onnx` (for launch tests)
331+
- Class names file: `/workspaces/deep_ros/deep_object_detection/config/coco_classes.txt`
332+
- GPU access: Required for GPU and TensorRT backend tests (local only)
282333

283-
1. **MultiImage input only**: The node only supports MultiImage messages. Individual camera topics are not supported.
284-
2. **Compressed images only**: Only compressed images (sensor_msgs/CompressedImage) are supported. Raw images are not supported.
285-
3. **Fail-fast provider selection**: If the specified execution provider is unavailable, the node fails immediately. No automatic fallback to other providers.
286-
4. **No dynamic reconfiguration**: Parameters cannot be changed at runtime. Node must be reconfigured to change parameters.
287-
288-
## Troubleshooting
289-
290-
- **"No plugin loaded"**: Check that the backend plugin name is correct in the configuration
291-
- **"No model loaded"**: Verify the model path exists and is a valid ONNX file
292-
- **"Provider initialization failed"**: Check that the specified execution provider (tensorrt/cuda) is available and properly configured
293-
- **Lifecycle errors**: Ensure the node is properly configured before activation
294-
- **No detections**: Verify that the model output format matches the expected format, or enable auto-detection
295-
- **Plugin discovery issues**: Check that `deep_ort_backend_plugin` is built and sourced
296-
- **CUDA/TensorRT errors**: Ensure CUDA libraries and TensorRT (if using) are properly installed and accessible

deep_object_detection/config/generic_model_params.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ deep_object_detection_node:
22
ros__parameters:
33
model_path: "/workspaces/deep_ros/yolov8m.onnx"
44
class_names_path: "/workspaces/deep_ros/deep_object_detection/config/coco_classes.txt"
5+
input_topic: "/multi_camera_sync/multi_image_compressed"
56

67
model:
78
num_classes: 80
@@ -37,7 +38,7 @@ deep_object_detection_node:
3738
score_idx: 4
3839
class_idx: 5
3940

40-
max_batch_size: 3
41+
max_batch_size: 6
4142
queue_size: 10
4243

4344
preferred_provider: "tensorrt"
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025-present WATonomous. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Launch test for deep_object_detection with CPU backend (cpu)."""
17+
18+
import os
19+
import time
20+
import unittest
21+
22+
import cv2
23+
import launch
24+
import launch_ros.actions
25+
import launch_testing
26+
import launch_testing.actions
27+
import launch_testing.asserts
28+
import numpy as np
29+
import pytest
30+
import rclpy
31+
from deep_msgs.msg import MultiImage
32+
from sensor_msgs.msg import CompressedImage
33+
from std_msgs.msg import Header
34+
from vision_msgs.msg import Detection2DArray
35+
36+
37+
@pytest.mark.launch_test
38+
def generate_test_description():
39+
"""Generate launch description for CPU backend test."""
40+
from ament_index_python.packages import get_package_share_directory
41+
42+
# Path to base config file
43+
config_file = os.path.join(
44+
get_package_share_directory("deep_object_detection"),
45+
"config",
46+
"generic_model_params.yaml",
47+
)
48+
49+
# Deep object detection node with CPU backend
50+
detection_node = launch_ros.actions.Node(
51+
package="deep_object_detection",
52+
executable="deep_object_detection_node",
53+
name="deep_object_detection_node",
54+
parameters=[
55+
config_file,
56+
{"preferred_provider": "cpu"},
57+
{"enable_trt_engine_cache": False},
58+
],
59+
output="screen",
60+
)
61+
62+
# Lifecycle manager
63+
lifecycle_manager = launch_ros.actions.Node(
64+
package="nav2_lifecycle_manager",
65+
executable="lifecycle_manager",
66+
name="lifecycle_manager",
67+
parameters=[{"node_names": ["deep_object_detection_node"], "autostart": True}],
68+
output="screen",
69+
)
70+
71+
return (
72+
launch.LaunchDescription(
73+
[detection_node, lifecycle_manager, launch_testing.actions.ReadyToTest()]
74+
),
75+
{
76+
"detection_node": detection_node,
77+
"lifecycle_manager": lifecycle_manager,
78+
},
79+
)
80+
81+
82+
class TestCPUBackend(unittest.TestCase):
83+
"""Test CPU backend functionality."""
84+
85+
@classmethod
86+
def setUpClass(cls):
87+
"""Initialize ROS context."""
88+
rclpy.init()
89+
90+
@classmethod
91+
def tearDownClass(cls):
92+
"""Shutdown ROS context."""
93+
rclpy.shutdown()
94+
95+
def setUp(self):
96+
"""Set up test fixtures."""
97+
self.node = rclpy.create_node("test_cpu_backend")
98+
99+
def tearDown(self):
100+
"""Clean up test fixtures."""
101+
self.node.destroy_node()
102+
103+
def test_node_starts(self, proc_output):
104+
"""Test that the detection node starts successfully."""
105+
proc_output.assertWaitFor("Deep object detection node created", timeout=10)
106+
107+
def test_backend_loads(self, proc_output):
108+
"""Test that CPU backend loads."""
109+
proc_output.assertWaitFor("Initialized backend using provider: cpu", timeout=15)
110+
111+
def test_model_loads(self, proc_output):
112+
"""Test that the model loads successfully."""
113+
# Model loading happens during backend initialization
114+
# Check for configuration completion which indicates model is loaded
115+
proc_output.assertWaitFor("Deep object detection node configured", timeout=20)
116+
117+
def test_node_activates(self, proc_output):
118+
"""Test that the node activates successfully."""
119+
proc_output.assertWaitFor(
120+
"Deep object detection node activated", timeout=20
121+
)
122+
123+
def test_detection_with_dummy_multiimage(self, proc_output):
124+
"""Test end-to-end detection by publishing a dummy MultiImage and verifying output."""
125+
# Wait for node to be fully activated
126+
proc_output.assertWaitFor(
127+
"Deep object detection node activated", timeout=20
128+
)
129+
time.sleep(1)
130+
131+
# Create publisher for MultiImage messages
132+
multi_image_pub = self.node.create_publisher(
133+
MultiImage, "/multi_camera_sync/multi_image_compressed", 10
134+
)
135+
136+
# Variable to track if we received detections
137+
received_detections = []
138+
139+
def detection_callback(msg):
140+
received_detections.append(msg)
141+
self.node.get_logger().info(
142+
f"Received detection output with {len(msg.detections)} detections"
143+
)
144+
145+
# Create subscriber for detection output with matching QoS
146+
from rclpy.qos import QoSProfile, SensorDataQoS
147+
qos_profile = SensorDataQoS()
148+
self.detection_sub = self.node.create_subscription(
149+
Detection2DArray, "/detections", detection_callback, qos_profile
150+
)
151+
152+
# Wait for publisher/subscriber to be ready
153+
time.sleep(2)
154+
155+
# Create a dummy compressed image (640x640 RGB JPEG)
156+
# This matches the expected input size from the config
157+
dummy_image = np.zeros((640, 640, 3), dtype=np.uint8)
158+
dummy_image[:, :] = [128, 128, 128] # Gray image
159+
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 90]
160+
_, encoded_image = cv2.imencode(".jpg", dummy_image, encode_param)
161+
162+
# Create MultiImage message with 1 image (for testing)
163+
# Note: max_batch_size is 6, but node will process when batch timer fires
164+
multi_image_msg = MultiImage()
165+
multi_image_msg.header = Header()
166+
multi_image_msg.header.stamp = self.node.get_clock().now().to_msg()
167+
multi_image_msg.header.frame_id = "camera"
168+
169+
# Create compressed image message
170+
compressed_img = CompressedImage()
171+
compressed_img.header = multi_image_msg.header
172+
compressed_img.format = "jpeg"
173+
compressed_img.data = encoded_image.tobytes()
174+
175+
multi_image_msg.images = [compressed_img]
176+
177+
# Publish MultiImage message multiple times to fill batch
178+
# (max_batch_size=6, but node processes when timer fires with >= max_batch_size)
179+
self.node.get_logger().info("Publishing dummy MultiImage for CPU detection test")
180+
for _ in range(6):
181+
multi_image_pub.publish(multi_image_msg)
182+
time.sleep(0.01) # Small delay between publishes
183+
184+
# Spin to process callbacks and wait for batch processing
185+
start_time = time.time()
186+
timeout = 15.0
187+
while len(received_detections) == 0 and (time.time() - start_time) < timeout:
188+
rclpy.spin_once(self.node, timeout_sec=0.1)
189+
190+
# Verify we received detections (even if empty, the message should be published)
191+
self.assertGreater(
192+
len(received_detections),
193+
0,
194+
"Should receive detection output after publishing MultiImage",
195+
)
196+
self.node.get_logger().info(
197+
f"CPU detection test passed! Received {len(received_detections[0].detections)} detections"
198+
)

0 commit comments

Comments
 (0)