Skip to content

Commit 4ca06e6

Browse files
committed
ros2doctor: add and test node/parameter reporting
- Add and update tests: test_node.py, test_parameter.py, test_api.py, common.py - Add test coverage for node and parameter for ros2doctor report Signed-off-by: BhuvanB404<bhuvanb1408@gmail.com> Signed-off-by: BhuvanB404 <bhuvanb1408@gmail.com>
1 parent 5622c13 commit 4ca06e6

File tree

7 files changed

+577
-39
lines changed

7 files changed

+577
-39
lines changed

ros2doctor/ros2doctor/api/parameter.py

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515

16-
from rcl_interfaces.srv import ListParameters
1716
import rclpy
17+
from rclpy.parameter_client import AsyncParameterClient
1818
from ros2cli.node.direct import DirectNode
1919
from ros2cli.node.strategy import NodeStrategy
2020
from ros2doctor.api import DoctorCheck
@@ -24,33 +24,19 @@
2424
from ros2doctor.api.format import doctor_warn
2525

2626

27-
def call_list_parameters(node, node_name, namespace='/'):
28-
"""Call the list_parameters service for a specific node."""
27+
def call_list_parameters(node, node_name):
28+
"""Call the list_parameters service for a specific node using AsyncParameterClient."""
2929
try:
30-
# Create service name and client for the target node's list_parameters service
31-
service_name = f"{namespace.rstrip('/')}/{node_name}/list_parameters"
32-
if service_name.startswith('//'):
33-
service_name = service_name[1:]
34-
35-
client = node.create_client(ListParameters, service_name)
36-
37-
if not client.wait_for_service(timeout_sec=1.0):
30+
client = AsyncParameterClient(node, node_name)
31+
ready = client.wait_for_services(timeout_sec=2.0)
32+
if not ready:
3833
return None
39-
40-
request = ListParameters.Request()
41-
future = client.call_async(request)
42-
43-
# Spin until the service call completes or times out
34+
future = client.list_parameters()
4435
rclpy.spin_until_future_complete(node, future, timeout_sec=2.0)
45-
46-
if future.done():
47-
response = future.result()
48-
node.destroy_client(client)
49-
return response
50-
else:
51-
node.destroy_client(client)
52-
return None
53-
36+
if future.done() and future.result() is not None:
37+
# Return the parameter names list
38+
return future.result().result.names
39+
return None
5440
except Exception:
5541
return None
5642

@@ -70,9 +56,9 @@ def check(self):
7056
node_names_and_namespaces = []
7157
with DirectNode(None) as param_node:
7258
for node_name, namespace in node_names_and_namespaces:
73-
response = call_list_parameters(param_node.node, node_name, namespace)
74-
if response is None:
75-
full_name = f"{namespace.rstrip('/')}/{node_name}"
59+
full_name = f"{namespace.rstrip('/')}/{node_name}"
60+
param_names = call_list_parameters(param_node.node, full_name)
61+
if param_names is None:
7662
doctor_warn(f'Node {full_name} has no parameter services.')
7763
result.add_warning()
7864
return result
@@ -103,17 +89,14 @@ def report(self):
10389
with DirectNode(None) as param_node:
10490
for node_name, namespace in sorted(node_names_and_namespaces):
10591
nodes_checked += 1
106-
response = call_list_parameters(param_node.node, node_name, namespace)
107-
if response and hasattr(response, 'result') and response.result:
108-
result = response.result
109-
param_names = result.names if hasattr(result, 'names') else []
110-
if param_names:
111-
total_param_count += len(param_names)
112-
full_name = f"{namespace.rstrip('/')}/{node_name}"
113-
114-
report.add_to_report('node', full_name)
115-
for param_name in sorted(param_names):
116-
report.add_to_report('parameter', param_name)
92+
full_name = f"{namespace.rstrip('/')}/{node_name}"
93+
param_names = call_list_parameters(param_node.node, full_name)
94+
95+
if param_names:
96+
total_param_count += len(param_names)
97+
report.add_to_report('node', full_name)
98+
for param_name in sorted(param_names):
99+
report.add_to_report('parameter', param_name)
117100

118101
report.add_to_report('total nodes checked', nodes_checked)
119102
report.add_to_report('total parameter count', total_param_count)

ros2doctor/test/common.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,32 @@ def generate_expected_service_report(services: Iterable[str], serv_counts: Itera
3333
expected_report.add_to_report('service count', serv_count)
3434
expected_report.add_to_report('client count', cli_count)
3535
return expected_report
36+
37+
38+
def generate_expected_node_report(node_count: int, nodes: Iterable[str]) -> Report:
39+
expected_report = Report('NODE LIST')
40+
if node_count == 0:
41+
expected_report.add_to_report('node count', 0)
42+
expected_report.add_to_report('node', 'none')
43+
else:
44+
expected_report.add_to_report('node count', node_count)
45+
for node in nodes:
46+
expected_report.add_to_report('node', node)
47+
return expected_report
48+
49+
50+
def generate_expected_parameter_report(nodes_checked: int, param_count: int,
51+
node_params: Iterable[tuple]) -> Report:
52+
expected_report = Report('PARAMETER LIST')
53+
if nodes_checked == 0:
54+
expected_report.add_to_report('total nodes checked', 0)
55+
expected_report.add_to_report('total parameter count', 0)
56+
expected_report.add_to_report('parameter', 'none')
57+
else:
58+
for node_name, params in node_params:
59+
expected_report.add_to_report('node', node_name)
60+
for param in params:
61+
expected_report.add_to_report('parameter', param)
62+
expected_report.add_to_report('total nodes checked', nodes_checked)
63+
expected_report.add_to_report('total parameter count', param_count)
64+
return expected_report
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright 2019 Open Source Robotics Foundation, Inc.
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 rclpy
16+
from rclpy.action import ActionServer
17+
from rclpy.executors import ExternalShutdownException
18+
from rclpy.node import Node
19+
from rclpy.qos import qos_profile_system_default
20+
21+
from test_msgs.action import Fibonacci
22+
from test_msgs.msg import Arrays
23+
from test_msgs.msg import Strings
24+
from test_msgs.srv import BasicTypes
25+
26+
27+
class ComplexNode(Node):
28+
29+
def __init__(self):
30+
super().__init__('complex_node')
31+
self.publisher = self.create_publisher(Arrays, 'arrays', qos_profile_system_default)
32+
self.subscription = self.create_subscription(
33+
Strings, 'strings', lambda msg: None, qos_profile_system_default
34+
)
35+
self.server = self.create_service(BasicTypes, 'basic', lambda req, res: res)
36+
self.action_server = ActionServer(
37+
self, Fibonacci, 'fibonacci', self.action_callback
38+
)
39+
self.timer = self.create_timer(1.0, self.pub_callback)
40+
41+
def destroy_node(self):
42+
self.timer.destroy()
43+
self.publisher.destroy()
44+
self.subscription.destroy()
45+
self.server.destroy()
46+
self.action_server.destroy()
47+
super().destroy_node()
48+
49+
def pub_callback(self):
50+
self.publisher.publish(Arrays())
51+
52+
def action_callback(self, goal_handle):
53+
goal_handle.succeed()
54+
return Fibonacci.Result()
55+
56+
57+
def main(args=None):
58+
try:
59+
with rclpy.init(args=args):
60+
node = ComplexNode()
61+
rclpy.spin(node)
62+
except (KeyboardInterrupt, ExternalShutdownException):
63+
print('node stopped cleanly')
64+
65+
66+
if __name__ == '__main__':
67+
main()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2025 Open Source Robotics Foundation, Inc.
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+
"""Test fixture node with parameters for ros2doctor testing."""
16+
17+
import rclpy
18+
from rclpy.executors import ExternalShutdownException
19+
from rclpy.parameter import PARAMETER_SEPARATOR_STRING
20+
21+
22+
def main():
23+
try:
24+
with rclpy.init():
25+
node = rclpy.create_node('parameter_node')
26+
# Declare various parameter types for testing
27+
node.declare_parameter('bool_param', True)
28+
node.declare_parameter('int_param', 42)
29+
node.declare_parameter('double_param', 3.14)
30+
node.declare_parameter('str_param', 'hello')
31+
node.declare_parameter('bool_array_param', [True, False])
32+
node.declare_parameter('int_array_param', [1, 2, 3])
33+
node.declare_parameter('double_array_param', [1.0, 2.0])
34+
node.declare_parameter('str_array_param', ['foo', 'bar'])
35+
node.declare_parameter(
36+
'nested' + PARAMETER_SEPARATOR_STRING + 'param', 'nested_value'
37+
)
38+
rclpy.spin(node)
39+
except (KeyboardInterrupt, ExternalShutdownException):
40+
print('parameter_node stopped cleanly')
41+
42+
43+
if __name__ == '__main__':
44+
main()

ros2doctor/test/test_api.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import unittest
1616

17+
from common import generate_expected_node_report
1718
from common import generate_expected_service_report
1819
from common import generate_expected_topic_report
1920

@@ -25,6 +26,10 @@
2526

2627
import pytest
2728

29+
from ros2doctor.api.node import NodeCheck
30+
from ros2doctor.api.node import NodeReport
31+
from ros2doctor.api.parameter import ParameterCheck
32+
from ros2doctor.api.parameter import ParameterReport
2833
from ros2doctor.api.service import ServiceReport
2934
from ros2doctor.api.topic import TopicCheck
3035
from ros2doctor.api.topic import TopicReport
@@ -74,3 +79,29 @@ def test_no_service_report(self):
7479
self.assertEqual(report.name, expected_report.name)
7580
self.assertEqual(report.items, expected_report.items)
7681
self.assertEqual(report, expected_report)
82+
83+
def test_node_check(self):
84+
"""Assume no duplicate nodes exist in a clean environment."""
85+
node_check = NodeCheck()
86+
check_result = node_check.check()
87+
self.assertEqual(check_result.error, 0)
88+
self.assertEqual(check_result.warning, 0)
89+
90+
def test_no_node_report(self):
91+
"""Assume no nodes are running in a clean environment."""
92+
report = NodeReport().report()
93+
expected_report = generate_expected_node_report(0, [])
94+
self.assertEqual(report.name, expected_report.name)
95+
self.assertEqual(report.name, 'NODE LIST')
96+
97+
def test_parameter_check(self):
98+
"""Test parameter check runs without errors in clean environment."""
99+
param_check = ParameterCheck()
100+
check_result = param_check.check()
101+
self.assertEqual(check_result.error, 0)
102+
103+
def test_no_parameter_report(self):
104+
"""Assume no parameters in a clean environment."""
105+
report = ParameterReport().report()
106+
self.assertEqual(report.name, 'PARAMETER LIST')
107+
self.assertIsNotNone(report)

0 commit comments

Comments
 (0)