Skip to content

Commit f4ee809

Browse files
URJalamergify[bot]
authored andcommitted
Add test for hardware component lifecycle (#1476)
(cherry picked from commit f2a8380)
1 parent 5f03745 commit f4ee809

File tree

2 files changed

+181
-1
lines changed

2 files changed

+181
-1
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python
2+
# Copyright 2025, Universal Robots A/S
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions are met:
6+
#
7+
# * Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
#
10+
# * Redistributions in binary form must reproduce the above copyright
11+
# notice, this list of conditions and the following disclaimer in the
12+
# documentation and/or other materials provided with the distribution.
13+
#
14+
# * Neither the name of the {copyright_holder} nor the names of its
15+
# contributors may be used to endorse or promote products derived from
16+
# this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28+
# POSSIBILITY OF SUCH DAMAGE.
29+
30+
import os
31+
import sys
32+
import time
33+
import unittest
34+
35+
import pytest
36+
37+
import launch_testing
38+
import rclpy
39+
from rclpy.node import Node
40+
41+
from lifecycle_msgs.msg import State
42+
43+
sys.path.append(os.path.dirname(__file__))
44+
from test_common import ( # noqa: E402
45+
ControllerManagerInterface,
46+
DashboardInterface,
47+
IoStatusInterface,
48+
generate_driver_test_description,
49+
)
50+
51+
52+
@pytest.mark.launch_test
53+
@launch_testing.parametrize(
54+
"tf_prefix",
55+
[""],
56+
)
57+
def generate_test_description(tf_prefix):
58+
return generate_driver_test_description(tf_prefix=tf_prefix)
59+
60+
61+
class ComponentLifecycleTest(unittest.TestCase):
62+
@classmethod
63+
def setUpClass(cls):
64+
# Initialize the ROS context
65+
rclpy.init()
66+
cls.node = Node("component_lifecycle_test")
67+
time.sleep(1)
68+
cls.init_robot(cls)
69+
70+
@classmethod
71+
def tearDownClass(cls):
72+
# Shutdown the ROS context
73+
cls.node.destroy_node()
74+
rclpy.shutdown()
75+
76+
def init_robot(self):
77+
self._dashboard_interface = DashboardInterface(self.node)
78+
self._controller_manager_interface = ControllerManagerInterface(self.node)
79+
self._io_status_controller_interface = IoStatusInterface(self.node)
80+
81+
def setUp(self):
82+
self._dashboard_interface.start_robot()
83+
time.sleep(1)
84+
self.assertTrue(self._io_status_controller_interface.resend_robot_program().success)
85+
86+
#
87+
# Tests
88+
#
89+
90+
def test_component_lifecycle(self):
91+
hardware_info = self._controller_manager_interface.list_hardware_components()
92+
self.assertIsNotNone(hardware_info)
93+
self.assertEqual(len(hardware_info.component), 1)
94+
self.assertEqual(hardware_info.component[0].state.id, State.PRIMARY_STATE_ACTIVE)
95+
component_name = hardware_info.component[0].name
96+
97+
command_interfaces = hardware_info.component[0].command_interfaces
98+
state_interfaces = hardware_info.component[0].state_interfaces
99+
100+
# Check that all interfaces are available after startup
101+
for interface in command_interfaces:
102+
self.assertTrue(interface.is_available, f"Interface {interface.name} is not available")
103+
for interface in state_interfaces:
104+
self.assertTrue(interface.is_available, f"Interface {interface.name} is not available")
105+
106+
self.assertTrue(
107+
self._controller_manager_interface.set_hardware_component_state(
108+
name=component_name, target_state=State(id=State.PRIMARY_STATE_INACTIVE)
109+
).ok
110+
)
111+
time.sleep(2)
112+
113+
# Check all interfaces are available after deactivation (This is current behavior, but docs say they should not be?)
114+
hardware_info = self._controller_manager_interface.list_hardware_components()
115+
self.assertIsNotNone(hardware_info)
116+
self.assertEqual(hardware_info.component[0].state.id, State.PRIMARY_STATE_INACTIVE)
117+
command_interfaces = hardware_info.component[0].command_interfaces
118+
state_interfaces = hardware_info.component[0].state_interfaces
119+
for interface in command_interfaces:
120+
self.assertTrue(
121+
interface.is_available,
122+
f"Interface {interface.name} is not available after deactivation",
123+
)
124+
for interface in state_interfaces:
125+
self.assertTrue(
126+
interface.is_available,
127+
f"Interface {interface.name} is not available after deactivation",
128+
)
129+
130+
self.assertTrue(
131+
self._controller_manager_interface.set_hardware_component_state(
132+
name=component_name, target_state=State(id=State.PRIMARY_STATE_UNCONFIGURED)
133+
).ok
134+
)
135+
time.sleep(2)
136+
137+
# Check all interfaces are unavailable after shutdown of hardware
138+
hardware_info = self._controller_manager_interface.list_hardware_components()
139+
self.assertIsNotNone(hardware_info)
140+
self.assertEqual(hardware_info.component[0].state.id, State.PRIMARY_STATE_UNCONFIGURED)
141+
command_interfaces = hardware_info.component[0].command_interfaces
142+
state_interfaces = hardware_info.component[0].state_interfaces
143+
for interface in command_interfaces:
144+
self.assertFalse(
145+
interface.is_available, f"Interface {interface.name} is available after shutdown"
146+
)
147+
for interface in state_interfaces:
148+
self.assertFalse(
149+
interface.is_available, f"Interface {interface.name} is available after shutdown"
150+
)
151+
152+
self.assertTrue(
153+
self._controller_manager_interface.set_hardware_component_state(
154+
name=component_name, target_state=State(id=State.PRIMARY_STATE_ACTIVE)
155+
).ok
156+
)
157+
time.sleep(2)
158+
159+
# Check all interfaces are available after reactivation
160+
hardware_info = self._controller_manager_interface.list_hardware_components()
161+
self.assertIsNotNone(hardware_info)
162+
self.assertEqual(hardware_info.component[0].state.id, State.PRIMARY_STATE_ACTIVE)
163+
command_interfaces = hardware_info.component[0].command_interfaces
164+
state_interfaces = hardware_info.component[0].state_interfaces
165+
for interface in command_interfaces:
166+
self.assertTrue(
167+
interface.is_available,
168+
f"Interface {interface.name} is not available after reactivation",
169+
)
170+
for interface in state_interfaces:
171+
self.assertTrue(
172+
interface.is_available,
173+
f"Interface {interface.name} is not available after reactivation",
174+
)

ur_robot_driver/test/test_common.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
SwitchController,
3535
LoadController,
3636
UnloadController,
37+
SetHardwareComponentState,
38+
ListHardwareComponents,
3739
)
3840
from launch import LaunchDescription
3941
from launch.actions import (
@@ -254,7 +256,11 @@ class ControllerManagerInterface(
254256
"load_controller": LoadController,
255257
"unload_controller": UnloadController,
256258
},
257-
services={"list_controllers": ListControllers},
259+
services={
260+
"list_controllers": ListControllers,
261+
"set_hardware_component_state": SetHardwareComponentState,
262+
"list_hardware_components": ListHardwareComponents,
263+
},
258264
):
259265
def wait_for_controller(self, controller_name, target_state=None, timeout=TIMEOUT_WAIT_SERVICE):
260266
start_time = time.time()

0 commit comments

Comments
 (0)