Skip to content

Commit c492156

Browse files
committed
SLORETZ: can run some composition tests
Signed-off-by: Shane Loretz <[email protected]>
1 parent f1e4ee1 commit c492156

File tree

4 files changed

+285
-61
lines changed

4 files changed

+285
-61
lines changed

test_launch_ros/setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[develop]
2+
script-dir=$base/lib/test_launch_ros
3+
[install]
4+
install-scripts=$base/lib/test_launch_ros

test_launch_ros/test/rostest/composition.py

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 os
16+
import unittest
17+
18+
from composition_interfaces.srv import LoadNode
19+
from launch import LaunchDescription
20+
from launch import LaunchService
21+
from launch.actions import ExecuteProcess
22+
from launch.actions import OpaqueFunction
23+
from launch.actions import RegisterEventHandler
24+
from launch.event_handlers.on_process_start import OnProcessStart
25+
from launch_ros import get_default_launch_description
26+
from launch_ros.actions import ComposableNodeContainer
27+
from launch_ros.actions import LoadComposableNodes
28+
from launch_ros.descriptions import ComposableNode
29+
import launch_testing
30+
import launch_testing.asserts
31+
from rcl_interfaces.msg import Parameter
32+
from rcl_interfaces.msg import ParameterType
33+
34+
35+
def generate_test_description(ready_fn):
36+
# Necessary to get real-time stdout from python processes:
37+
proc_env = os.environ.copy()
38+
proc_env['PYTHONUNBUFFERED'] = '1'
39+
40+
launch_description = LaunchDescription()
41+
42+
mock_container = ComposableNodeContainer(
43+
env=proc_env,
44+
node_name='my_container',
45+
node_namespace='/my_ns',
46+
package='test_launch_ros',
47+
node_executable='mock-composable-container',
48+
composable_node_descriptions=[
49+
ComposableNode(package='fake_package', node_plugin='successfully_load'),
50+
ComposableNode(package='fake_package', node_plugin='fail_to_load'),
51+
ComposableNode(
52+
package='fake_package', node_plugin='node_name',
53+
node_name='my_talker'
54+
),
55+
ComposableNode(
56+
package='fake_package', node_plugin='node_namespace',
57+
node_namespace='my_namespace'
58+
),
59+
ComposableNode(
60+
package='fake_package', node_plugin='remap_rules',
61+
remappings=[('~/foo', '/bar')]
62+
),
63+
ComposableNode(
64+
package='fake_package', node_plugin='parameters',
65+
parameters=[{'foo': {'bar': 'baz'}}]
66+
),
67+
ComposableNode(
68+
package='fake_package', node_plugin='extra_arguments',
69+
extra_arguments=[{'ping.pong': 5}]
70+
),
71+
# TODO(sloretz) log level
72+
# ComposableNode(
73+
# package='fake_package', node_plugin='log_level',
74+
# log_level=1
75+
# ),
76+
])
77+
78+
launch_description.add_action(get_default_launch_description())
79+
launch_description.add_action(mock_container)
80+
# TODO(sloretz) post-launch composable node actions
81+
# launch_description.add_action(
82+
# RegisterEventHandler(
83+
# event_handler=OnProcessStart(
84+
# target_action=mock_container,
85+
# on_start=[
86+
# LoadComposableNodes(
87+
# composable_node_descriptions=[
88+
# ComposableNode(
89+
# package='fake_package', node_plugin='node_name',
90+
# node_name='my_talker'
91+
# ),
92+
# ],
93+
# target_container=mock_container
94+
# )
95+
# ]
96+
# )
97+
# )
98+
# )
99+
launch_description.add_action(
100+
OpaqueFunction(function=lambda context: ready_fn())
101+
)
102+
103+
return launch_description, {'container': mock_container}
104+
105+
106+
class TestComposition(unittest.TestCase):
107+
108+
def test_successfully_load(self, container):
109+
request = LoadNode.Request()
110+
request.package_name = 'fake_package'
111+
request.plugin_name = 'successfully_load'
112+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
113+
114+
def test_fail_to_load(self, container):
115+
request = LoadNode.Request()
116+
request.package_name = 'fake_package'
117+
request.plugin_name = 'fail_to_load'
118+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
119+
120+
def test_custom_node_name(self, container):
121+
request = LoadNode.Request()
122+
request.package_name = 'fake_package'
123+
request.plugin_name = 'node_name'
124+
request.node_name = 'my_talker'
125+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
126+
127+
def test_custom_node_namespace(self, container):
128+
request = LoadNode.Request()
129+
request.package_name = 'fake_package'
130+
request.plugin_name = 'node_namespace'
131+
request.node_namespace = 'my_namespace'
132+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
133+
134+
def test_custom_remap_rules(self, container):
135+
request = LoadNode.Request()
136+
request.package_name = 'fake_package'
137+
request.plugin_name = 'remap_rules'
138+
request.remap_rules = ['~/foo:=/bar']
139+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
140+
141+
def test_custom_parameters(self, container):
142+
request = LoadNode.Request()
143+
request.package_name = 'fake_package'
144+
request.plugin_name = 'parameters'
145+
p = Parameter()
146+
p.name = 'foo.bar'
147+
p.value.string_value = 'baz'
148+
p.value.type = ParameterType.PARAMETER_STRING
149+
request.parameters = [p]
150+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
151+
152+
def test_custom_extra_arguments(self, container):
153+
request = LoadNode.Request()
154+
request.package_name = 'fake_package'
155+
request.plugin_name = 'extra_arguments'
156+
p = Parameter()
157+
p.name = 'ping.pong'
158+
p.value.integer_value = 5
159+
p.value.type = ParameterType.PARAMETER_INTEGER
160+
request.extra_arguments = [p]
161+
self.proc_output.assertWaitFor(expected_output=repr(request), process=container)
162+
163+
# TODO(sloretz) log level
164+
# def test_custom_log_level(self, container):
165+
# request = LoadNode.Request()
166+
# request.package_name = 'fake_package'
167+
# request.plugin_name = 'log_level'
168+
# request.log_level = 1
169+
# self.proc_output.assertWaitFor(expected_output=repr(request), process=container)

test_launch_ros/test_launch_ros/mock_composable_container.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,116 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import sys
16+
17+
from composition_interfaces.srv import LoadNode
18+
import rclpy
19+
from rclpy.node import Node
20+
21+
22+
class MockComposableNodeContainer(Node):
23+
24+
def __enter__(self):
25+
self.__load_service = self.create_service(
26+
LoadNode, '~/_container/load_node', self._on_load_node)
27+
return self
28+
29+
def __exit__(self, type_, value, traceback):
30+
self.destroy_service(self.__load_service)
31+
self.destroy_node()
32+
33+
def __init__(self, name, namespace):
34+
"""
35+
Initialize a mock container process.
36+
37+
:param load_node_responses: responses to a load node request for a given package/plugin
38+
:type load_node_responses: {(str, str): composition_interfaces.LoadNode.Response, ...}
39+
"""
40+
super().__init__(name, namespace=namespace)
41+
42+
# Canned responses to load service
43+
fail_to_load = LoadNode.Response()
44+
fail_to_load.success = False
45+
fail_to_load.error_message = 'intentional failure'
46+
47+
successfully_load = LoadNode.Response()
48+
successfully_load.success = True
49+
successfully_load.full_node_name = '/a_nodename'
50+
successfully_load.unique_id = 2
51+
52+
node_name = LoadNode.Response()
53+
node_name.success = True
54+
node_name.full_node_name = '/my_talker'
55+
node_name.unique_id = 4
56+
57+
node_namespace = LoadNode.Response()
58+
node_namespace.success = True
59+
node_namespace.full_node_name = '/my_namespace/my_talker'
60+
node_namespace.unique_id = 8
61+
62+
log_level = LoadNode.Response()
63+
log_level.success = True
64+
log_level.full_node_name = '/a_nodename'
65+
log_level.unique_id = 16
66+
67+
remap_rules = LoadNode.Response()
68+
remap_rules.success = True
69+
remap_rules.full_node_name = '/a_nodename'
70+
remap_rules.unique_id = 32
71+
72+
parameters = LoadNode.Response()
73+
parameters.success = True
74+
parameters.full_node_name = '/a_nodename'
75+
parameters.unique_id = 64
76+
77+
extra_arguments = LoadNode.Response()
78+
extra_arguments.success = True
79+
extra_arguments.full_node_name = '/a_nodename'
80+
extra_arguments.unique_id = 128
81+
82+
self.__load_node_responses = {
83+
('fake_package', 'fail_to_load'): fail_to_load,
84+
('fake_package', 'successfully_load'): successfully_load,
85+
('fake_package', 'node_name'): node_name,
86+
('fake_package', 'node_namespace'): node_namespace,
87+
('fake_package', 'log_level'): log_level,
88+
('fake_package', 'remap_rules'): remap_rules,
89+
('fake_package', 'parameters'): parameters,
90+
('fake_package', 'extra_arguments'): extra_arguments,
91+
}
92+
93+
self.unexpected_request = False
94+
95+
def _on_load_node(self, request, response):
96+
key = (request.package_name, request.plugin_name)
97+
if key not in self.__load_node_responses:
98+
self.unexpected_request = True
99+
unexpected_load = LoadNode.Response()
100+
unexpected_load.success = False
101+
unexpected_load.error_message = 'unexpected load request'
102+
return response
103+
else:
104+
print(repr(request))
105+
return self.__load_node_responses[key]
106+
return response
107+
108+
15109
def main():
16-
print('Hello world')
110+
rclpy.init()
111+
container = MockComposableNodeContainer(name='mock_container', namespace='/')
112+
with container:
113+
try:
114+
rclpy.spin(container)
115+
except KeyboardInterrupt:
116+
print('Got SIGINT, shutting down')
117+
except:
118+
import traceback
119+
traceback.print_exc()
120+
if container.unexpected_request:
121+
sys.stderr.write('failing due to unexpected request\n')
122+
sys.exit(1)
123+
rclpy.shutdown()
124+
125+
126+
if __name__ == '__main__':
127+
main()

0 commit comments

Comments
 (0)