Skip to content

Commit fe37d9a

Browse files
authored
Running integration tests with mock hardware (#1226)
1 parent f2a8380 commit fe37d9a

12 files changed

+983
-508
lines changed

ur_robot_driver/CMakeLists.txt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ if(BUILD_TESTING)
230230
find_package(ur_msgs REQUIRED)
231231
find_package(launch_testing_ament_cmake)
232232

233+
add_launch_test(test/test_mock_hardware.py
234+
TIMEOUT
235+
800
236+
)
237+
233238
if(${UR_ROBOT_DRIVER_BUILD_INTEGRATION_TESTS})
234239
add_launch_test(test/launch_args.py
235240
TIMEOUT
@@ -243,7 +248,7 @@ if(BUILD_TESTING)
243248
TIMEOUT
244249
180
245250
)
246-
add_launch_test(test/robot_driver.py
251+
add_launch_test(test/integration_test_scaled_joint_controller.py
247252
TIMEOUT
248253
800
249254
)
@@ -259,6 +264,18 @@ if(BUILD_TESTING)
259264
TIMEOUT
260265
500
261266
)
267+
add_launch_test(test/integration_test_config_controller.py
268+
TIMEOUT
269+
800
270+
)
271+
add_launch_test(test/integration_test_passthrough_controller.py
272+
TIMEOUT
273+
800
274+
)
275+
add_launch_test(test/integration_test_io_controller.py
276+
TIMEOUT
277+
800
278+
)
262279
add_launch_test(test/integration_test_tool_contact.py
263280
TIMEOUT
264281
800
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 launch_testing
36+
import pytest
37+
import rclpy
38+
from rclpy.node import Node
39+
40+
sys.path.append(os.path.dirname(__file__))
41+
from test_common import ( # noqa: E402
42+
ControllerManagerInterface,
43+
DashboardInterface,
44+
IoStatusInterface,
45+
ConfigurationInterface,
46+
generate_driver_test_description,
47+
)
48+
49+
50+
@pytest.mark.launch_test
51+
@launch_testing.parametrize("tf_prefix", [(""), ("my_ur_")])
52+
def generate_test_description(tf_prefix):
53+
return generate_driver_test_description(tf_prefix=tf_prefix)
54+
55+
56+
class ConfigControllerTest(unittest.TestCase):
57+
@classmethod
58+
def setUpClass(cls):
59+
# Initialize the ROS context
60+
rclpy.init()
61+
cls.node = Node("config_controller_test")
62+
time.sleep(1)
63+
cls.init_robot(cls)
64+
65+
@classmethod
66+
def tearDownClass(cls):
67+
# Shutdown the ROS context
68+
cls.node.destroy_node()
69+
rclpy.shutdown()
70+
71+
def init_robot(self):
72+
self._dashboard_interface = DashboardInterface(self.node)
73+
self._controller_manager_interface = ControllerManagerInterface(self.node)
74+
self._io_status_controller_interface = IoStatusInterface(self.node)
75+
self._configuration_controller_interface = ConfigurationInterface(self.node)
76+
77+
def setUp(self):
78+
self._dashboard_interface.start_robot()
79+
time.sleep(1)
80+
self.assertTrue(self._io_status_controller_interface.resend_robot_program().success)
81+
82+
#
83+
# Test functions
84+
#
85+
86+
def test_get_robot_software_version(self):
87+
self.assertGreater(
88+
self._configuration_controller_interface.get_robot_software_version().major, 1
89+
)

ur_robot_driver/test/integration_test_controller_switch.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,17 @@
6060

6161

6262
@pytest.mark.launch_test
63-
@launch_testing.parametrize(
64-
"tf_prefix",
65-
[""],
66-
# [(""), ("my_ur_")],
67-
)
63+
@launch_testing.parametrize("tf_prefix", [(""), ("my_ur_")])
6864
def generate_test_description(tf_prefix):
6965
return generate_driver_test_description(tf_prefix=tf_prefix)
7066

7167

72-
class RobotDriverTest(unittest.TestCase):
68+
class ControllerSwitchTest(unittest.TestCase):
7369
@classmethod
7470
def setUpClass(cls):
7571
# Initialize the ROS context
7672
rclpy.init()
77-
cls.node = Node("robot_driver_test")
73+
cls.node = Node("controller_switching_test")
7874
time.sleep(1)
7975
cls.init_robot(cls)
8076

ur_robot_driver/test/integration_test_force_mode.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,12 @@ def generate_test_description(tf_prefix):
8888
return generate_driver_test_description(tf_prefix=tf_prefix)
8989

9090

91-
class RobotDriverTest(unittest.TestCase):
91+
class ForceModeTest(unittest.TestCase):
9292
@classmethod
9393
def setUpClass(cls):
9494
# Initialize the ROS context
9595
rclpy.init()
96-
cls.node = Node("robot_driver_test")
96+
cls.node = Node("force_mode_test")
9797
time.sleep(1)
9898
cls.init_robot(cls)
9999

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 logging
31+
import os
32+
import sys
33+
import time
34+
import unittest
35+
36+
import launch_testing
37+
import pytest
38+
import rclpy
39+
from rclpy.node import Node
40+
from ur_msgs.msg import IOStates
41+
42+
sys.path.append(os.path.dirname(__file__))
43+
from test_common import ( # noqa: E402
44+
ControllerManagerInterface,
45+
DashboardInterface,
46+
IoStatusInterface,
47+
generate_driver_test_description,
48+
)
49+
50+
51+
@pytest.mark.launch_test
52+
@launch_testing.parametrize("tf_prefix", [(""), ("my_ur_")])
53+
def generate_test_description(tf_prefix):
54+
return generate_driver_test_description(tf_prefix=tf_prefix)
55+
56+
57+
class IOControllerTest(unittest.TestCase):
58+
@classmethod
59+
def setUpClass(cls):
60+
# Initialize the ROS context
61+
rclpy.init()
62+
cls.node = Node("io_controller_test")
63+
time.sleep(1)
64+
cls.init_robot(cls)
65+
66+
@classmethod
67+
def tearDownClass(cls):
68+
# Shutdown the ROS context
69+
cls.node.destroy_node()
70+
rclpy.shutdown()
71+
72+
def init_robot(self):
73+
self._dashboard_interface = DashboardInterface(self.node)
74+
self._controller_manager_interface = ControllerManagerInterface(self.node)
75+
self._io_status_controller_interface = IoStatusInterface(self.node)
76+
77+
def setUp(self):
78+
self._dashboard_interface.start_robot()
79+
time.sleep(1)
80+
self.assertTrue(self._io_status_controller_interface.resend_robot_program().success)
81+
82+
#
83+
# Test functions
84+
#
85+
86+
def test_set_io(self):
87+
"""Test to set an IO and check whether it has been set."""
88+
# Create io callback to verify result
89+
io_msg = None
90+
91+
def io_msg_cb(msg):
92+
nonlocal io_msg
93+
io_msg = msg
94+
95+
io_states_sub = self.node.create_subscription(
96+
IOStates,
97+
"/io_and_status_controller/io_states",
98+
io_msg_cb,
99+
rclpy.qos.qos_profile_system_default,
100+
)
101+
102+
# Set pin 0 to 1.0
103+
test_pin = 0
104+
105+
logging.info("Setting pin %d to 1.0", test_pin)
106+
self._io_status_controller_interface.set_io(fun=1, pin=test_pin, state=1.0)
107+
108+
# Wait until the pin state has changed
109+
pin_state = False
110+
end_time = time.time() + 5
111+
while not pin_state and time.time() < end_time:
112+
rclpy.spin_once(self.node, timeout_sec=0.1)
113+
if io_msg is not None:
114+
pin_state = io_msg.digital_out_states[test_pin].state
115+
116+
self.assertEqual(pin_state, 1.0)
117+
118+
# Set pin 0 to 0.0
119+
logging.info("Setting pin %d to 0.0", test_pin)
120+
self._io_status_controller_interface.set_io(fun=1, pin=test_pin, state=0.0)
121+
122+
# Wait until the pin state has changed back
123+
end_time = time.time() + 5
124+
while pin_state and time.time() < end_time:
125+
rclpy.spin_once(self.node, timeout_sec=0.1)
126+
if io_msg is not None:
127+
pin_state = io_msg.digital_out_states[test_pin].state
128+
129+
self.assertEqual(pin_state, 0.0)
130+
131+
# Clean up io subscription
132+
self.node.destroy_subscription(io_states_sub)

0 commit comments

Comments
 (0)