diff --git a/sw_watchdog/CMakeLists.txt b/sw_watchdog/CMakeLists.txt
index 78f2c97..93b4ecd 100644
--- a/sw_watchdog/CMakeLists.txt
+++ b/sw_watchdog/CMakeLists.txt
@@ -59,6 +59,21 @@ if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
set(ament_cmake_copyright_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
+
+ # launch tests
+ find_package(launch_testing_ament_cmake REQUIRED)
+ file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test/test_watchdog_shutdown_heartbeat.py" SHUTDOWN_SCRIPT)
+ file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/test/test_watchdog_expected_output" EXPECTED_OUTPUT)
+ configure_file(
+ "test/test_watchdog.launch.py.in"
+ "test/test_watchdog.launch.py"
+ @ONLY)
+ add_launch_test(
+ "${CMAKE_CURRENT_BINARY_DIR}/test/test_watchdog.launch.py"
+ TARGET "test_watchdog"
+ TIMEOUT 30
+ ENV
+ )
endif()
install(DIRECTORY
diff --git a/sw_watchdog/package.xml b/sw_watchdog/package.xml
index a67540f..ee50099 100644
--- a/sw_watchdog/package.xml
+++ b/sw_watchdog/package.xml
@@ -6,6 +6,7 @@
Library of software watchdogs implemented as ROS 2 lifecycle nodes
Philipp Robbel
Apache License 2.0
+ Arne Nordmann
ament_cmake
@@ -14,14 +15,19 @@
sw_watchdog_msgs
rclcpp_lifecycle
+ launch
lifecycle_msgs
ros2run
sw_watchdog_msgs
ament_lint_auto
ament_lint_common
- ros_testing
-
+ launch
+ launch_ros
+ launch_testng
+ lifecycle_msgs
+ rclpy
+ ros2component
ament_cmake
diff --git a/sw_watchdog/test/test_watchdog.launch.py.in b/sw_watchdog/test/test_watchdog.launch.py.in
new file mode 100644
index 0000000..d4abfc7
--- /dev/null
+++ b/sw_watchdog/test/test_watchdog.launch.py.in
@@ -0,0 +1,123 @@
+# Copyright (c) 2021 Robert Bosch GmbH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+
+import unittest
+
+import ament_index_python
+import launch
+import launch_ros
+import launch_testing
+import launch_testing.actions
+import launch_testing.asserts
+import launch_testing.util
+import launch_testing_ros
+
+from launch import LaunchDescription
+from launch.actions import ExecuteProcess
+from launch.actions import LogInfo
+from launch.actions import RegisterEventHandler
+from launch.actions import Shutdown
+from launch.events import matches_action
+from launch.events.process import ShutdownProcess
+from launch_ros.actions import ComposableNodeContainer
+from launch_ros.actions import LifecycleNode
+from launch_ros.descriptions import ComposableNode
+from launch_ros.event_handlers import OnStateTransition
+
+import lifecycle_msgs.msg
+
+
+def generate_test_description():
+ os.environ['OSPL_VERBOSITY'] = '8'
+ os.environ['RCUTILS_CONSOLE_OUTPUT_FORMAT'] = '{message}'
+
+ container = ComposableNodeContainer(
+ name='my_container',
+ namespace='my_namespace',
+ package='rclcpp_components',
+ executable='component_container',
+ composable_node_descriptions=[
+ ComposableNode(
+ package='demo_nodes_cpp',
+ plugin='demo_nodes_cpp::Talker',
+ name='talker'),
+ ComposableNode(
+ package='sw_watchdog',
+ plugin='sw_watchdog::SimpleHeartbeat',
+ name='heartbeat',
+ parameters=[{'period': 200}],
+ extra_arguments=[{'use_intra_process_comms': True}]),
+ ],
+ output='screen')
+
+ watchdog_node = LifecycleNode(
+ package='sw_watchdog',
+ executable='simple_watchdog',
+ namespace='',
+ name='simple_watchdog',
+ output='screen',
+ arguments=['220', '--publish', '--activate'])
+
+ watchdog_inactive_handler = RegisterEventHandler(
+ OnStateTransition(
+ target_lifecycle_node=watchdog_node,
+ goal_state='inactive',
+ entities=[LogInfo(msg='Watchdog transitioned to `INACTIVE` state.')],
+ )
+ )
+
+ shutdown_heartbeat_when_watchdog_active = ExecuteProcess(
+ cmd=[
+ "@PYTHON_EXECUTABLE@",
+ "@SHUTDOWN_SCRIPT@"
+ ],
+ name='shutdown_heartbeat_when_watchdog_active',
+ emulate_tty=True)
+
+ launch_description = LaunchDescription()
+ launch_description.add_action(container)
+ launch_description.add_action(watchdog_node)
+ launch_description.add_action(watchdog_inactive_handler)
+ launch_description.add_action(shutdown_heartbeat_when_watchdog_active)
+ launch_description.add_action(launch_testing.util.KeepAliveProc())
+ launch_description.add_action(launch_testing.actions.ReadyToTest())
+
+ return launch_description, locals()
+
+class TestModeManagement(unittest.TestCase):
+
+ def test_processes_output(self, proc_output, watchdog_node):
+ """Check manager and nodes logging output for expected strings."""
+
+ from launch_testing.tools.output import get_default_filtered_prefixes
+ output_filter = launch_testing_ros.tools.basic_output_filter(
+ filtered_prefixes=get_default_filtered_prefixes() + ['service not available, waiting...'],
+ filtered_rmw_implementation='@rmw_implementation@'
+ )
+ proc_output.assertWaitFor(
+ expected_output=launch_testing.tools.expected_output_from_file(path='@EXPECTED_OUTPUT@'),
+ process=watchdog_node,
+ output_filter=output_filter,
+ timeout=15
+ )
+
+ import time
+ time.sleep(1)
+
+@launch_testing.post_shutdown_test()
+class TestModeManagementShutdown(unittest.TestCase):
+
+ def test_last_process_exit_code(self, proc_info, watchdog_node):
+ launch_testing.asserts.assertExitCodes(proc_info, process=watchdog_node)
diff --git a/sw_watchdog/test/test_watchdog_expected_output.regex b/sw_watchdog/test/test_watchdog_expected_output.regex
new file mode 100644
index 0000000..830e157
--- /dev/null
+++ b/sw_watchdog/test/test_watchdog_expected_output.regex
@@ -0,0 +1,3 @@
+Watchdog raised, heartbeat sent at
+Publishing lease expiry.*missed count
+on_deactivate.*called
diff --git a/sw_watchdog/test/test_watchdog_shutdown_heartbeat.py b/sw_watchdog/test/test_watchdog_shutdown_heartbeat.py
new file mode 100644
index 0000000..1e94d31
--- /dev/null
+++ b/sw_watchdog/test/test_watchdog_shutdown_heartbeat.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2021 Robert Bosch GmbH
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import time
+
+from lifecycle_msgs.srv import GetState
+import rclpy
+from ros2component.api import get_components_in_container, unload_component_from_container
+
+
+def main(args=None):
+ rclpy.init(args=args)
+
+ node = rclpy.create_node('shutdown_heartbeat')
+ cli = node.create_client(GetState, 'simple_watchdog/get_state')
+
+ while not cli.wait_for_service(timeout_sec=1.0):
+ print('simple watchdog not yet available, waiting...')
+
+ active = False
+ req = GetState.Request()
+ while not active:
+ future = cli.call_async(req)
+ rclpy.spin_until_future_complete(node, future)
+ if future.result() is not None:
+ time.sleep(1)
+ node.get_logger().info(
+ 'Watchdog active, so now shutting down talker and heartbeat to test watchdog.')
+ active = True
+
+ container = 'my_namespace/my_container'
+ comps = get_components_in_container(node=node, remote_container_node_name=container)
+ print(comps)
+ for uid, error, reason in unload_component_from_container(
+ node=node,
+ remote_container_node_name=container,
+ component_uids=[1, 2]):
+ if error:
+ return 'Failed to unload component {} from `{}` container node\n {}'.format(
+ uid, container, reason.capitalize()
+ )
+ print('Unloaded component {} from `{}` container node'.format(
+ uid, container
+ ))
+ else:
+ node.get_logger().error('Exception while calling service: %r' % future.exception())
+ break
+
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/sw_watchdog_msgs/package.xml b/sw_watchdog_msgs/package.xml
index 536ade7..ea4a32b 100644
--- a/sw_watchdog_msgs/package.xml
+++ b/sw_watchdog_msgs/package.xml
@@ -16,8 +16,6 @@
ament_lint_auto
ament_lint_common
- ros_testing
-
rosidl_interface_packages