Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 56 additions & 16 deletions ros2action/ros2action/verb/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,35 @@
import rclpy
from ros2action.api import action_name_completer
from ros2action.api import get_action_clients_and_servers
from ros2action.api import get_action_names_and_types
from ros2action.verb import VerbExtension
from ros2cli.node.strategy import add_arguments as add_strategy_node_arguments
from ros2cli.node.strategy import NodeStrategy


class InfoVerb(VerbExtension):
"""Print information about an action."""

def add_arguments(self, parser, cli_name):
add_strategy_node_arguments(parser)
arg = parser.add_argument(
'action_name',
help="Name of the ROS action to get info (e.g. '/fibonacci')")
arg.completer = action_name_completer
parser.add_argument(
'-t', '--show-types', action='store_true',
help='Additionally show the action type')
help='Additionally show the action type next to each listed '
'action client and action server')
parser.add_argument(
'-c', '--count', action='store_true',
help='Only display the number of action clients and action servers')
Comment thread
fujitatomoya marked this conversation as resolved.
parser.add_argument(
'--verbose',
'-v',
action='store_true',
help='Prints detailed information for each action client and action server of '
'this action, including the endpoint information of the underlying goal, '
'cancel, and result services and feedback and status topics')

def main(self, *, args):
with NodeStrategy(args) as node:
Expand All @@ -44,20 +55,49 @@ def main(self, *, args):
except (ValueError, rclpy.exceptions.InvalidTopicNameException) as e:
raise RuntimeError(e)

print('Action: {}'.format(args.action_name))
print('Action clients: {}'.format(len(action_clients)))
if not args.count:
for client_name, client_types in action_clients:
if args.show_types:
types_formatted = ', '.join(client_types)
print(f' {client_name} [{types_formatted}]')
action_names_and_types = get_action_names_and_types(node=node)
action_types = None
for (a_name, a_types) in action_names_and_types:
if a_name == args.action_name:
action_types = a_types
break

line_end = '\n'
if args.verbose:
line_end = '\n\n'

print('Action: {}'.format(args.action_name))
if action_types is not None:
print('Type: %s' % ', '.join(action_types))

Comment thread
fujitatomoya marked this conversation as resolved.
print('Action clients: {}'.format(len(action_clients)))
if not args.count:
if args.verbose:
try:
for info in node.get_action_clients_info_by_action(args.action_name):
print(info, end=line_end)
except NotImplementedError as e:
return str(e)
else:
print(f' {client_name}')
print('Action servers: {}'.format(len(action_servers)))
if not args.count:
for server_name, server_types in action_servers:
if args.show_types:
types_formatted = ', '.join(server_types)
print(f' {server_name} [{types_formatted}]')
for client_name, client_types in action_clients:
if args.show_types:
types_formatted = ', '.join(client_types)
print(f' {client_name} [{types_formatted}]')
else:
print(f' {client_name}')

print('Action servers: {}'.format(len(action_servers)))
if not args.count:
if args.verbose:
try:
for info in node.get_action_servers_info_by_action(args.action_name):
print(info, end=line_end)
except NotImplementedError as e:
return str(e)
else:
print(f' {server_name}')
for server_name, server_types in action_servers:
if args.show_types:
types_formatted = ', '.join(server_types)
print(f' {server_name} [{types_formatted}]')
else:
print(f' {server_name}')
57 changes: 57 additions & 0 deletions ros2action/test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def test_fibonacci_info(self):
assert launch_testing.tools.expect_output(
expected_lines=[
'Action: /test/fibonacci',
'Type: test_msgs/action/Fibonacci',
'Action clients: 0',
'Action servers: 1',
' /test/fibonacci_action_server'
Expand All @@ -189,6 +190,7 @@ def test_fibonacci_info_with_types(self):
assert launch_testing.tools.expect_output(
expected_lines=[
'Action: /test/fibonacci',
'Type: test_msgs/action/Fibonacci',
'Action clients: 0',
'Action servers: 1',
' /test/fibonacci_action_server [test_msgs/action/Fibonacci]'
Expand All @@ -206,13 +208,68 @@ def test_fibonacci_info_count(self):
assert launch_testing.tools.expect_output(
expected_lines=[
'Action: /test/fibonacci',
'Type: test_msgs/action/Fibonacci',
'Action clients: 0',
'Action servers: 1',
],
text=action_command.output,
strict=False
)

@launch_testing.markers.retry_on_failure(times=5, delay=1)
def test_fibonacci_info_verbose(self):
with self.launch_action_command(
arguments=['info', '-v', '/test/fibonacci']) as action_command:
assert action_command.wait_for_shutdown(timeout=10)
assert action_command.exit_code == launch_testing.asserts.EXIT_OK
assert launch_testing.tools.expect_output(
expected_lines=[
'Action: /test/fibonacci',
'Type: test_msgs/action/Fibonacci',
'Action clients: 0',
'Action servers: 1',
'Node name: fibonacci_action_server',
'Node namespace: /test',
'Action type: test_msgs/action/Fibonacci',
'Endpoint type: SERVER',
'Goal service:',
' Service type: test_msgs/action/Fibonacci_SendGoal',
re.compile(r'\s*Service type hash: RIHS01_[a-f0-9]{64}'),
re.compile(r'\s*Endpoint count: [12]'),
re.compile(r'\s*Reliability: RELIABLE'),
'Cancel service:',
' Service type: action_msgs/srv/CancelGoal',
'Result service:',
' Service type: test_msgs/action/Fibonacci_GetResult',
'Feedback topic:',
' Topic type: test_msgs/action/Fibonacci_FeedbackMessage',
'Status topic:',
' Topic type: action_msgs/msg/GoalStatusArray',
re.compile(r'\s*Durability: TRANSIENT_LOCAL'),
],
text=action_command.output,
strict=False
)

@launch_testing.markers.retry_on_failure(times=5, delay=1)
def test_fibonacci_info_verbose_count(self):
with self.launch_action_command(
arguments=['info', '-v', '-c', '/test/fibonacci']) as action_command:
assert action_command.wait_for_shutdown(timeout=10)
assert action_command.exit_code == launch_testing.asserts.EXIT_OK
assert launch_testing.tools.expect_output(
expected_lines=[
'Action: /test/fibonacci',
'Type: test_msgs/action/Fibonacci',
'Action clients: 0',
'Action servers: 1',
],
text=action_command.output,
strict=False
)
# The count option should suppress the detailed endpoint information
assert 'Node name:' not in action_command.output

@launch_testing.markers.retry_on_failure(times=5, delay=1)
def test_list(self):
with self.launch_action_command(arguments=['list']) as action_command:
Expand Down
6 changes: 5 additions & 1 deletion ros2cli/ros2cli/daemon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,14 @@ def serve(server: LocalXMLRPCServer, *, timeout: float = 2 * 60 * 60):
node.get_client_names_and_types_by_node,
node.get_action_server_names_and_types_by_node,
node.get_action_client_names_and_types_by_node,
node.get_action_clients_info_by_action,
node.get_action_servers_info_by_action,
node.count_publishers,
node.count_subscribers,
node.count_clients,
node.count_services
node.count_services,
node.count_action_clients,
node.count_action_servers
]

# Dealing with the timeouts in this server is a bit tricky. The caller
Expand Down
6 changes: 6 additions & 0 deletions ros2cli/ros2cli/xmlrpc/marshal/rclpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ def dump_duration(marshaller, value, write):
Marshaller.dispatch[rclpy.endpoint_info.ServiceEndpointInfo] = \
functools.partial(dump_any_with_slots, transform=lambda slot: slot.lstrip('_'))

Unmarshaller.dispatch[fullname(rclpy.endpoint_info.ActionEndpointInfo)] = \
functools.partial(end_any_with_slots, type_=rclpy.endpoint_info.ActionEndpointInfo)

Marshaller.dispatch[rclpy.endpoint_info.ActionEndpointInfo] = \
functools.partial(dump_any_with_slots, transform=lambda slot: slot.lstrip('_'))

Unmarshaller.dispatch[fullname(rclpy.endpoint_info.EndpointTypeEnum)] = \
functools.partial(end_any_enum, enum_=rclpy.endpoint_info.EndpointTypeEnum)

Expand Down
56 changes: 56 additions & 0 deletions ros2cli/test/test_ros2cli_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,54 @@ def test_get_servers_info_by_service(daemon_node):
TEST_SERVICE_SERVER_QOS.reliability


def test_get_action_clients_info_by_action(daemon_node):
clients_info = daemon_node.get_action_clients_info_by_action(TEST_ACTION_NAME)
assert len(clients_info) == 1
test_client_info = clients_info[0]
assert test_client_info.node_name == TEST_NODE_NAME
assert test_client_info.node_namespace == TEST_NODE_NAMESPACE
assert test_client_info.action_type == TEST_ACTION_TYPE
assert test_client_info.endpoint_type == EndpointTypeEnum.CLIENT
goal_info = test_client_info.goal_service_info
assert goal_info.service_type == TEST_ACTION_TYPE + '_SendGoal'
assert goal_info.endpoint_type == EndpointTypeEnum.CLIENT
assert (goal_info.endpoint_count == 1 or goal_info.endpoint_count == 2)
assert len(goal_info.qos_profiles) == goal_info.endpoint_count
assert len(goal_info.endpoint_gids) == goal_info.endpoint_count
assert test_client_info.cancel_service_info.service_type == 'action_msgs/srv/CancelGoal'
assert test_client_info.result_service_info.service_type == TEST_ACTION_TYPE + '_GetResult'
feedback_info = test_client_info.feedback_topic_info
assert feedback_info.topic_type == TEST_ACTION_TYPE + '_FeedbackMessage'
assert feedback_info.endpoint_type == EndpointTypeEnum.SUBSCRIPTION
status_info = test_client_info.status_topic_info
assert status_info.topic_type == 'action_msgs/msg/GoalStatusArray'
assert status_info.endpoint_type == EndpointTypeEnum.SUBSCRIPTION


def test_get_action_servers_info_by_action(daemon_node):
servers_info = daemon_node.get_action_servers_info_by_action(TEST_ACTION_NAME)
assert len(servers_info) == 1
test_server_info = servers_info[0]
assert test_server_info.node_name == TEST_NODE_NAME
assert test_server_info.node_namespace == TEST_NODE_NAMESPACE
assert test_server_info.action_type == TEST_ACTION_TYPE
assert test_server_info.endpoint_type == EndpointTypeEnum.SERVER
goal_info = test_server_info.goal_service_info
assert goal_info.service_type == TEST_ACTION_TYPE + '_SendGoal'
assert goal_info.endpoint_type == EndpointTypeEnum.SERVER
assert (goal_info.endpoint_count == 1 or goal_info.endpoint_count == 2)
assert len(goal_info.qos_profiles) == goal_info.endpoint_count
assert len(goal_info.endpoint_gids) == goal_info.endpoint_count
assert test_server_info.cancel_service_info.service_type == 'action_msgs/srv/CancelGoal'
assert test_server_info.result_service_info.service_type == TEST_ACTION_TYPE + '_GetResult'
feedback_info = test_server_info.feedback_topic_info
assert feedback_info.topic_type == TEST_ACTION_TYPE + '_FeedbackMessage'
assert feedback_info.endpoint_type == EndpointTypeEnum.PUBLISHER
status_info = test_server_info.status_topic_info
assert status_info.topic_type == 'action_msgs/msg/GoalStatusArray'
assert status_info.endpoint_type == EndpointTypeEnum.PUBLISHER


def test_count_publishers(daemon_node):
assert 1 == daemon_node.count_publishers(TEST_TOPIC_NAME)

Expand All @@ -320,6 +368,14 @@ def test_count_services(daemon_node):
assert 1 == daemon_node.count_services(TEST_SERVICE_NAME)


def test_count_action_clients(daemon_node):
assert 1 == daemon_node.count_action_clients(TEST_ACTION_NAME)


def test_count_action_servers(daemon_node):
assert 1 == daemon_node.count_action_servers(TEST_ACTION_NAME)


def _wait_until(predicate, timeout):
# Polling is_daemon_running() uses system.listMethods, which is not a
# timer-resetting RPC, so it never keeps the daemon alive artificially.
Expand Down