diff --git a/.cspell.json b/.cspell.json
index 4c14cb9f3..83b4a3dc2 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -48,6 +48,7 @@
"stds",
"struct",
"structs",
+ "TOLOWER",
"UDP_SEQ",
"usec",
"vccint",
diff --git a/nebula_ros/CMakeLists.txt b/nebula_ros/CMakeLists.txt
index 709de2025..b13cf4006 100644
--- a/nebula_ros/CMakeLists.txt
+++ b/nebula_ros/CMakeLists.txt
@@ -224,7 +224,19 @@ install(DIRECTORY include/ DESTINATION include/${PROJECT_NAME})
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
+ find_package(ros_testing REQUIRED)
+
ament_lint_auto_find_test_dependencies()
+
+ foreach(MODEL Pandar40P Pandar64 PandarQT64 PandarQT128 Pandar128E4X PandarAT128 PandarXT32 PandarXT32M)
+ string(TOLOWER ${MODEL}_smoke_test test_name)
+ add_ros_test(
+ test/smoke_test.py
+ TARGET ${test_name}
+ ARGS "launch_file_path:=${PROJECT_SOURCE_DIR}/launch/nebula_launch.py" "sensor_model:=${MODEL}"
+ TIMEOUT "10"
+ )
+ endforeach()
endif()
ament_export_include_directories("include/${PROJECT_NAME}")
diff --git a/nebula_ros/package.xml b/nebula_ros/package.xml
index 3a97db1f6..334a5f3da 100644
--- a/nebula_ros/package.xml
+++ b/nebula_ros/package.xml
@@ -11,6 +11,7 @@
ament_cmake_auto
ros_environment
+ ros_testing
continental_msgs
continental_srvs
@@ -34,6 +35,7 @@
ament_cmake_gtest
ament_lint_auto
+ ros_testing
ament_cmake
diff --git a/nebula_ros/test/smoke_test.py b/nebula_ros/test/smoke_test.py
new file mode 100644
index 000000000..7e6484723
--- /dev/null
+++ b/nebula_ros/test/smoke_test.py
@@ -0,0 +1,56 @@
+import time
+import unittest
+
+from launch import LaunchContext
+from launch import LaunchDescription
+from launch.actions import IncludeLaunchDescription
+from launch.actions import OpaqueFunction
+from launch.launch_description_sources import PythonLaunchDescriptionSource
+from launch.substitutions import LaunchConfiguration
+import launch_testing
+import launch_testing.actions
+import launch_testing.asserts
+import pytest
+import rclpy
+
+
+def resolve_launch_file(context: LaunchContext, *args, **kwargs):
+ sensor_model = LaunchConfiguration("sensor_model").perform(context)
+ launch_file_path = LaunchConfiguration("launch_file_path").perform(context)
+
+ return [
+ IncludeLaunchDescription(
+ PythonLaunchDescriptionSource(launch_file_path),
+ launch_arguments=[("sensor_model", sensor_model), ("launch_hw", "false")],
+ )
+ ]
+
+
+@pytest.mark.launch_test
+def generate_test_description():
+ return LaunchDescription(
+ [OpaqueFunction(function=resolve_launch_file), launch_testing.actions.ReadyToTest()]
+ )
+
+
+class DummyTest(unittest.TestCase):
+ def test_wait_for_node_ready(self):
+ """Waiting for the node is ready."""
+ rclpy.init()
+ test_node = rclpy.create_node("test_node")
+ # Wait until both dummy node "test_node" and real tested node are registered and then kill
+ # both of them, if tested node does not register within `timeout` seconds test will fail
+ start_time = time.time()
+ timeout = 2 # seconds
+ timeout_msg = "Smoke test timeout has been reached ({}s)".format(timeout)
+ print("waiting for Nodes to be ready")
+ while len(test_node.get_node_names()) < 2:
+ assert time.time() - start_time < timeout, timeout_msg
+ time.sleep(0.1)
+ rclpy.shutdown()
+
+
+@launch_testing.post_shutdown_test()
+class TestYourNodeShutdown(unittest.TestCase):
+ def test_exit_code(self, proc_info):
+ launch_testing.asserts.assertExitCodes(proc_info)
diff --git a/nebula_tests/package.xml b/nebula_tests/package.xml
index f593b2e01..92d5456ee 100644
--- a/nebula_tests/package.xml
+++ b/nebula_tests/package.xml
@@ -15,6 +15,8 @@
diagnostic_updater
nebula_common
nebula_decoders
+ nebula_hw_interfaces
+ nebula_ros
rosbag2_cpp
ament_cmake_gtest