Skip to content

Commit 875d84c

Browse files
crystaldustsloretz
authored andcommitted
Add test cases for composition API.
Signed-off-by: Zhen Ju <[email protected]>
1 parent 9a43691 commit 875d84c

File tree

8 files changed

+336
-3
lines changed

8 files changed

+336
-3
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2020 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+
from rclpy.node import Node
16+
17+
18+
class TestFoo(Node):
19+
def __init__(self, node_name='test_foo', **kwargs):
20+
super().__init__(node_name, **kwargs)

rclpy_components/resource/test_composition

Whitespace-only changes.

rclpy_components/setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
('share/ament_index/resource_index/packages',
1111
['resource/' + package_name]),
1212
('share/' + package_name, ['package.xml']),
13+
('share/ament_index/resource_index/packages',
14+
['resource/' + 'test_composition'])
1315
],
1416
install_requires=['setuptools'],
1517
zip_safe=True,
@@ -23,5 +25,8 @@
2325
'component_container = rclpy_components.component_container:main',
2426
'component_container_mt = rclpy_components.component_container_mt:main',
2527
],
28+
'rclpy_components': [
29+
'test_composition::TestFoo = rclpy_components_test:TestFoo',
30+
]
2631
},
2732
)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Copyright 2020 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 unittest
16+
import rclpy
17+
from multiprocessing import Process
18+
from rclpy.client import Client
19+
from rclpy.executors import SingleThreadedExecutor, MultiThreadedExecutor
20+
from rclpy.node import Node
21+
from composition_interfaces.srv import ListNodes, LoadNode, UnloadNode
22+
from rclpy_components.component_manager import ComponentManager
23+
24+
TEST_COMPOSITION = 'test_composition'
25+
TEST_COMPOSITION_FOO = 'test_composition::TestFoo'
26+
TEST_COMPOSITION_NODE_NAME = '/testfoo'
27+
28+
29+
def run_container(container_name, multi_thread):
30+
rclpy.init()
31+
if multi_thread:
32+
executor = MultiThreadedExecutor()
33+
else:
34+
executor = SingleThreadedExecutor()
35+
36+
component_manager = ComponentManager(executor, container_name)
37+
executor.add_node(component_manager)
38+
try:
39+
executor.spin()
40+
except Exception as e:
41+
print(e)
42+
pass
43+
44+
component_manager.destroy_node()
45+
rclpy.shutdown()
46+
47+
48+
class TestComponentManager(unittest.TestCase):
49+
helper_node: Node = None
50+
container_process: Process = None
51+
52+
container_name = 'TestComponentManager'
53+
# service names & clients will be generated with container_name
54+
load_node_svc_name = ""
55+
unload_node_svc_name = ""
56+
list_node_svc_name = ""
57+
load_cli: Client = None
58+
unload_cli: Client = None
59+
list_cli: Client = None
60+
61+
use_multi_threaded_executor = False
62+
63+
@classmethod
64+
def setUpClass(cls):
65+
cls.load_node_svc_name = f'{cls.container_name}/_container/load_node'
66+
cls.unload_node_svc_name = f'{cls.container_name}/_container/unload_node'
67+
cls.list_node_svc_name = f'{cls.container_name}/_container/list_nodes'
68+
69+
# Start the test component manager in the background
70+
cls.container_process = Process(target=run_container,
71+
args=(cls.container_name, cls.use_multi_threaded_executor))
72+
cls.container_process.start()
73+
74+
# Setup the helper_node, which will help create client and test the services
75+
rclpy.init()
76+
cls.helper_node = rclpy.create_node('helper_node')
77+
cls.load_cli = cls.helper_node.create_client(LoadNode, cls.load_node_svc_name)
78+
cls.unload_cli = cls.helper_node.create_client(UnloadNode, cls.unload_node_svc_name)
79+
cls.list_cli = cls.helper_node.create_client(ListNodes, cls.list_node_svc_name)
80+
81+
@classmethod
82+
def tearDownClass(cls):
83+
cls.container_process.terminate()
84+
cls.load_cli.destroy()
85+
cls.unload_cli.destroy()
86+
cls.list_cli.destroy()
87+
cls.helper_node.destroy_node()
88+
rclpy.shutdown()
89+
90+
@classmethod
91+
def load_node(cls, package_name, plugin_name, node_name="", node_namespace=""):
92+
if not cls.load_cli.wait_for_service(timeout_sec=5.0):
93+
raise RuntimeError(f'No load service found in /{cls.container_name}')
94+
95+
load_req = LoadNode.Request()
96+
load_req.package_name = package_name
97+
load_req.plugin_name = plugin_name
98+
99+
if node_name != "":
100+
load_req.node_name = node_name
101+
if node_namespace != "":
102+
load_req.node_namespace = node_namespace
103+
104+
future = cls.load_cli.call_async(load_req)
105+
rclpy.spin_until_future_complete(cls.helper_node, future)
106+
return future.result()
107+
108+
@classmethod
109+
def unload_node(cls, unique_id):
110+
if not cls.unload_cli.wait_for_service(timeout_sec=5.0):
111+
raise RuntimeError(f'No unload service found in /{cls.container_name}')
112+
113+
unload_req = UnloadNode.Request()
114+
unload_req.unique_id = unique_id
115+
116+
future = cls.unload_cli.call_async(unload_req)
117+
rclpy.spin_until_future_complete(cls.helper_node, future)
118+
return future.result()
119+
120+
@classmethod
121+
def list_nodes(cls):
122+
if not cls.list_cli.wait_for_service(timeout_sec=5.0):
123+
raise RuntimeError(f'No list service found in {cls.container_name}')
124+
list_req = ListNodes.Request()
125+
future = cls.list_cli.call_async(list_req)
126+
rclpy.spin_until_future_complete(cls.helper_node, future)
127+
return future.result()
128+
129+
def load_node_test(self):
130+
load_res = self.__class__.load_node(TEST_COMPOSITION, TEST_COMPOSITION_FOO)
131+
assert load_res.success is True
132+
assert load_res.error_message == ""
133+
assert load_res.unique_id == 1
134+
assert load_res.full_node_name == TEST_COMPOSITION_NODE_NAME
135+
136+
node_name = "renamed_node"
137+
node_ns = 'testing_ns'
138+
remap_load_res = self.__class__.load_node(
139+
TEST_COMPOSITION, TEST_COMPOSITION_FOO, node_name=node_name,
140+
node_namespace=node_ns)
141+
assert remap_load_res.success is True
142+
assert remap_load_res.error_message == ""
143+
assert remap_load_res.unique_id == 2
144+
assert remap_load_res.full_node_name == f'/{node_ns}/{node_name}'
145+
146+
list_res: ListNodes.Response = self.__class__.list_nodes()
147+
assert len(list_res.unique_ids) == len(list_res.full_node_names) == 2
148+
149+
def unload_node_test(self):
150+
if not self.__class__.unload_cli.wait_for_service(timeout_sec=5.0):
151+
raise RuntimeError(f'no unload service found in {self.__class__.container_name}')
152+
153+
unload_res: UnloadNode.Response = self.__class__.unload_node(1)
154+
assert unload_res.success is True
155+
assert unload_res.error_message == ""
156+
157+
# Should be only 1 node left
158+
list_res: ListNodes.Response = self.__class__.list_nodes()
159+
assert len(list_res.full_node_names) == len(list_res.unique_ids) == 1
160+
161+
# The index definitely won't exist
162+
unload_res: UnloadNode.Response = self.__class__.unload_node(1000)
163+
assert unload_res.error_message != ""
164+
assert unload_res.success is False
165+
list_res: ListNodes.Response = self.__class__.list_nodes()
166+
assert len(list_res.full_node_names) == len(list_res.unique_ids) == 1
167+
168+
# Unload the last node
169+
unload_req = UnloadNode.Request()
170+
unload_req.unique_id = 2
171+
future = self.__class__.unload_cli.call_async(unload_req)
172+
rclpy.spin_until_future_complete(self.__class__.helper_node, future)
173+
unload_res: UnloadNode.Response = future.result()
174+
assert unload_res.success is True
175+
assert unload_res.error_message == ""
176+
177+
# Won't be any node in the container
178+
list_res: ListNodes.Response = self.__class__.list_nodes()
179+
assert len(list_res.full_node_names) == len(list_res.unique_ids) == 0
180+
181+
def list_nodes_test(self):
182+
container_name = self.__class__.container_name
183+
print(f'{container_name}: list_nodes tested within test_load_node and test_unload_node')
184+
185+
def test_composition_api(self):
186+
# Control the order of test
187+
self.load_node_test()
188+
self.unload_node_test()
189+
self.list_nodes_test()
190+
191+
192+
class TestComponentManagerMT(TestComponentManager):
193+
use_multi_threaded_executor = True
194+
container_name = 'TestComponentManagerMT'
195+
196+
197+
if __name__ == '__main__':
198+
unittest.main()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2020 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 unittest
16+
import rclpy
17+
from composition_interfaces.srv import ListNodes, LoadNode, UnloadNode
18+
from rclpy.executors import Executor, SingleThreadedExecutor, MultiThreadedExecutor
19+
from rclpy_components.component_manager import ComponentManager
20+
21+
TEST_COMPOSITION = 'test_composition'
22+
TEST_COMPOSITION_FOO = 'test_composition::TestFoo'
23+
24+
25+
class TestComponentManagerUT(unittest.TestCase):
26+
executor: Executor
27+
component_manager: ComponentManager
28+
use_multi_threaded_executor: bool = False
29+
container_name: str = "ut_container"
30+
31+
@classmethod
32+
def setUpClass(cls) -> None:
33+
rclpy.init()
34+
35+
if cls.use_multi_threaded_executor:
36+
cls.executor = MultiThreadedExecutor()
37+
else:
38+
cls.executor = SingleThreadedExecutor()
39+
40+
cls.executor = SingleThreadedExecutor()
41+
cls.component_manager = ComponentManager(cls.executor, cls.container_name)
42+
cls.executor.add_node(cls.component_manager)
43+
44+
@classmethod
45+
def tearDownClass(cls) -> None:
46+
cls.executor.remove_node(cls.component_manager)
47+
cls.executor.shutdown()
48+
rclpy.shutdown()
49+
50+
def list_nodes(self):
51+
res = ListNodes.Response()
52+
req = ListNodes.Request()
53+
self.__class__.component_manager.on_list_node(req, res)
54+
55+
return res.unique_ids, res.full_node_names
56+
57+
def test_gen_unique_id(self):
58+
current_index = self.component_manager.gen_unique_id()
59+
assert current_index == 1 # The unique id start from 1
60+
61+
def test_list_node(self):
62+
unique_ids, full_node_names = self.list_nodes()
63+
assert len(full_node_names) == 0
64+
assert len(unique_ids) == 0
65+
66+
def test_load_node(self):
67+
mock_res = LoadNode.Response()
68+
mock_req = LoadNode.Request()
69+
mock_req.package_name = TEST_COMPOSITION
70+
mock_req.plugin_name = TEST_COMPOSITION_FOO
71+
self.component_manager.on_load_node(mock_req, mock_res)
72+
73+
print(mock_res.success, mock_res.error_message)
74+
75+
unique_ids, full_node_names = self.list_nodes()
76+
assert len(unique_ids) == 1
77+
assert len(full_node_names) == 1
78+
79+
def test_unload_node(self):
80+
mock_res = UnloadNode.Response()
81+
mock_req = UnloadNode.Request()
82+
83+
# Unload a non-existing node
84+
mock_req.unique_id = 0
85+
self.component_manager.on_unload_node(mock_req, mock_res)
86+
assert mock_res.error_message
87+
assert (not mock_res.success)
88+
89+
# Unload the first node
90+
ids, node_names = self.list_nodes()
91+
mock_req.unique_id = ids[0]
92+
# Don't forget to remove the previous test results
93+
mock_res = UnloadNode.Response()
94+
self.component_manager.on_unload_node(mock_req, mock_res)
95+
assert mock_res.success
96+
assert (not mock_res.error_message)
97+
98+
# There should be (n-1) nodes left
99+
unique_ids, full_node_names = self.list_nodes()
100+
assert len(unique_ids) == len(ids) - 1
101+
assert len(full_node_names) == len(node_names) - 1
102+
103+
104+
class TestComponentManagerUTMT(TestComponentManagerUT):
105+
use_multi_threaded_executor = True
106+
container_name = 'ut_component_mt'
107+
108+
109+
if __name__ == '__main__':
110+
unittest.main()

rclpy_components/test/test_copyright.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2015 Open Source Robotics Foundation, Inc.
1+
# Copyright 2020 Open Source Robotics Foundation, Inc.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

rclpy_components/test/test_flake8.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2017 Open Source Robotics Foundation, Inc.
1+
# Copyright 2020 Open Source Robotics Foundation, Inc.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

rclpy_components/test/test_pep257.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2015 Open Source Robotics Foundation, Inc.
1+
# Copyright 2020 Open Source Robotics Foundation, Inc.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)