Skip to content

Commit 420abbf

Browse files
authored
rqt_cm: Robustify test and fix shutdown races (backport #3396) (#3464)
1 parent 6dbcd10 commit 420abbf

3 files changed

Lines changed: 93 additions & 8 deletions

File tree

rqt_controller_manager/package.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<exec_depend>rqt_gui</exec_depend>
2828
<exec_depend>rqt_gui_py</exec_depend>
2929

30+
<test_depend>launch_testing</test_depend>
31+
<test_depend>launch_testing_ros</test_depend>
32+
3033
<export>
3134
<build_type>ament_python</build_type>
3235
<rqt_gui plugin="${prefix}/plugin.xml"/>

rqt_controller_manager/rqt_controller_manager/controller_manager.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def __init__(self, context):
146146
def shutdown_plugin(self):
147147
self._update_cm_list_timer.stop()
148148
self._update_ctrl_list_timer.stop()
149+
self._update_hw_components_list_timer.stop()
149150
self._popup_widget.hide()
150151

151152
def save_settings(self, plugin_settings, instance_settings):
@@ -164,7 +165,13 @@ def restore_settings(self, plugin_settings, instance_settings):
164165
pass
165166

166167
def _update_cm_list(self):
167-
update_combo(self._widget.cm_combo, _list_controller_managers(self._node))
168+
try:
169+
update_combo(self._widget.cm_combo, _list_controller_managers(self._node))
170+
except Exception as e:
171+
if "destruction was requested" in str(e) or "context is not valid" in str(e):
172+
self._update_cm_list_timer.stop()
173+
return
174+
raise
168175

169176
def _on_cm_change(self, cm_name):
170177
self._cm_name = cm_name
@@ -178,7 +185,13 @@ def _update_controllers(self):
178185
return
179186

180187
# Find controllers associated to the selected controller manager
181-
controllers = self._list_controllers()
188+
try:
189+
controllers = self._list_controllers()
190+
except Exception as e:
191+
if "destruction was requested" in str(e) or "context is not valid" in str(e):
192+
self._update_ctrl_list_timer.stop()
193+
return
194+
raise
182195

183196
# Update controller display, if necessary
184197
if self._controllers != controllers:
@@ -321,7 +334,13 @@ def _update_hw_components(self):
321334
return
322335

323336
# Find hw_components associated to the selected controller manager
324-
hw_components = self._list_hw_components()
337+
try:
338+
hw_components = self._list_hw_components()
339+
except Exception as e:
340+
if "destruction was requested" in str(e) or "context is not valid" in str(e):
341+
self._update_hw_components_list_timer.stop()
342+
return
343+
raise
325344

326345
# Update controller display, if necessary
327346
if self._hw_components != hw_components:
@@ -635,11 +654,16 @@ def _list_controller_managers(node):
635654
@return List of controller manager node names
636655
@rtype list of str
637656
"""
638-
return [
639-
name.rstrip("list_controllers").rstrip("/")
640-
for name, _ in get_service_names_and_types(node=node)
641-
if name.endswith("list_controllers")
642-
]
657+
try:
658+
return [
659+
name.rstrip("list_controllers").rstrip("/")
660+
for name, _ in get_service_names_and_types(node=node)
661+
if name.endswith("list_controllers")
662+
]
663+
except Exception as e:
664+
if "destruction was requested" in str(e) or "context is not valid" in str(e):
665+
return []
666+
raise
643667

644668

645669
def _get_parameter_controller_names(node, node_name):
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) 2026 Christian Rauch
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import time
16+
import unittest
17+
18+
import launch
19+
import launch_ros.actions
20+
import launch_testing.actions
21+
import launch_testing.markers
22+
import pytest
23+
import rclpy
24+
from rclpy.node import Node
25+
26+
node_name = "rqt_controller_manager_node"
27+
28+
29+
@pytest.mark.launch_test
30+
@launch_testing.markers.keep_alive
31+
def generate_test_description():
32+
return launch.LaunchDescription(
33+
[
34+
launch_ros.actions.Node(
35+
executable="rqt_controller_manager",
36+
package="rqt_controller_manager",
37+
name=node_name,
38+
arguments=["--force-discover"],
39+
additional_env={"QT_QPA_PLATFORM": "offscreen"},
40+
),
41+
launch_testing.actions.ReadyToTest(),
42+
]
43+
)
44+
45+
46+
class TestFixture(unittest.TestCase):
47+
48+
def setUp(self):
49+
rclpy.init()
50+
self.node = Node("test_node")
51+
52+
def tearDown(self):
53+
self.node.destroy_node()
54+
rclpy.shutdown()
55+
56+
def test_node_start(self, proc_output):
57+
time.sleep(2)
58+
assert node_name in self.node.get_node_names()

0 commit comments

Comments
 (0)