Skip to content

Commit df427e9

Browse files
hidmicpbaughman
andauthored
Update apex rostest. (#14)
* Give more info when output is not in expected sequence * Real captured output has newline in it. Update test data to reflect * Fix exception with process name resolution, add test * Add infrastructure to coordinate ROS_DOMAIN_IDs between apex_launchtest processes * Isolate tests with ROS_DOMAIN_ID by default * Address PR comments * Fix style - Defaults in the docker image we run the test in changed. We're now failing for style reasons. Mostly need to re-order imports and change " quotes to ' * apex_launchtest_ros add better example - Fix up style issues - Typo - Update apex_launchtest_ros/examples/README.md - Formatting - formatting - Fix bad spelling - Improve sentence - Fix Typo * Improve apex launchtest ros - Add the parametrization infrastrcture - Refactor the apex runner into an ApexRunner and a RunnerWorker - This veresion should still run existing tests. This is mostly a refactor commit * Refactor to allow for parametrized launch descriptions - Combine pre and post results into one set of results, add class names so the tests are distinguishable - Update tests to match new 'run' API that returns a dictionary of {test_run: result} instead of a tuple of (pre_shutdown, post_shutdown) results * Add parametrized example, finish refactor * Beef up test coverage for new features * Push changes into apex_rostest subdirectory. * Prune unnecesary files. Co-Authored-By: pbaughman <[email protected]> Signed-off-by: Pete Baughman <[email protected]> Signed-off-by: Michel Hidalgo <[email protected]>
1 parent c7f22b1 commit df427e9

File tree

9 files changed

+345
-7
lines changed

9 files changed

+345
-7
lines changed

apex_rostest/apex_launchtest_ros/apex_launchtest_ros/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
# limitations under the License.
1414

1515

16+
from .data_republisher import DataRepublisher
1617
from .message_pump import MessagePump
1718

1819

1920
__all__ = [
21+
'DataRepublisher',
2022
'MessagePump',
2123
]
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2019 Apex.AI, 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+
16+
class DataRepublisher:
17+
"""Republish mesasges with a transform function applied."""
18+
19+
def __init__(self, node, listen_topic, publish_topic, msg_type, transform_fn):
20+
"""
21+
Create a DataRepublisher.
22+
23+
:param node: A rclpy node that will run the publisher and subscriber
24+
25+
:param listen_topic: The topic to listen for incoming messages on
26+
27+
:param publish_topic: The topic to republish messages on
28+
29+
:param msg_type: The type of ROS msg to receive and republish
30+
31+
:param transfor_fn: A function that takes one mesasge and returns a new message to
32+
republish or None to drop the message
33+
"""
34+
self.__num_received = 0
35+
self.__republished_list = []
36+
37+
self.__node = node
38+
self.__subscriber = node.create_subscription(
39+
msg_type,
40+
listen_topic,
41+
callback=self.__cb
42+
)
43+
44+
self.__publisher = node.create_publisher(
45+
msg_type,
46+
publish_topic
47+
)
48+
49+
self.__transform_fn = transform_fn
50+
51+
def shutdown(self):
52+
"""Stop republishing messages."""
53+
self.__node.destroy_subscription(self.__subscriber)
54+
self.__node.destroy_publisher(self.__publisher)
55+
56+
def get_num_received(self):
57+
"""Get the number of messages received on the listen_topic."""
58+
return self.__num_received
59+
60+
def get_num_republished(self):
61+
"""
62+
Get the number of messages published on publish_topic.
63+
64+
This may be lower than get_num_received if the transform_fn indicated a message
65+
should be dropped
66+
"""
67+
return len(self.__republished_list)
68+
69+
def get_republished(self):
70+
"""Get a list of all of the transformed messages republished."""
71+
return self.__republished_list
72+
73+
def __cb(self, msg):
74+
self.__num_received += 1
75+
repub = self.__transform_fn(msg)
76+
77+
if repub:
78+
self.__republished_list.append(msg)
79+
self.__publisher.publish(repub)

apex_rostest/apex_launchtest_ros/apex_launchtest_ros/message_pump.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, node, context=None):
2525
self._node = node
2626
self._thread = threading.Thread(
2727
target=self._run,
28-
name="msg_pump_thread",
28+
name='msg_pump_thread',
2929
)
3030
self._run = True
3131
self._context = context
@@ -37,7 +37,7 @@ def stop(self):
3737
self._run = False
3838
self._thread.join(timeout=5.0)
3939
if self._thread.is_alive():
40-
raise Exception("Timed out waiting for message pump to stop")
40+
raise Exception('Timed out waiting for message pump to stop')
4141

4242
def _run(self):
4343
executor = rclpy.executors.SingleThreadedExecutor(context=self._context)
Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
# Examples
22

3-
## message_counter.launch.py
3+
## `talker_listener.test.py`
44

55
Usage:
6-
> apex_launchtest examples/message_counter.launch.py
6+
> apex_launchtest examples/talker_listener.test.py
77
8-
Launches a message counter node and runs a few tests to interact with the node
8+
This test launches the talker and listener example nodes from demo_nodes_py and interacts
9+
with them via their ROS interfaces. Remapping rules are used so that one of the tests can sit in
10+
between the talker and the listener and change the data on the fly.
11+
12+
Node that in the setUpClass method, the test makes sure that the listener is subscribed and
13+
republishing messages. Since the listener process provides no synchronization mechanism to
14+
inform the outside world that it's up and running, this step is necessary especially in resource
15+
constrained environments where process startup may take a non negligible amount of time. This
16+
is often the cause of "flakyness" in tests on CI systems. A more robust design of the talker and
17+
listener processes might provide some positive feedback that the node is up and running, but these
18+
are simple example nodes.
19+
20+
#### test_fuzzy_data
21+
This test gives an example of what a test that fuzzes data might look like. A ROS subscriber
22+
and publisher pair encapsulated in a `DataRepublisher` object changes the string "Hello World" to
23+
"Aloha World" as it travels between the talker and the listener.
24+
25+
#### test_listener_receives
26+
This test publishes unique messages on the `/chatter` topic and asserts that the same messages
27+
go to the stdout of the listener node
28+
29+
#### test_talker_transmits
30+
This test subscribes to the remapped `/talker_chatter` topic and makes sure the talker node also
31+
writes the data it's transmitting to stdout
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright 2019 Apex.AI, 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 time
17+
import unittest
18+
import uuid
19+
20+
from apex_launchtest.util import NoMatchingProcessException
21+
import apex_launchtest_ros
22+
import launch
23+
import launch_ros
24+
import launch_ros.actions
25+
import rclpy
26+
import rclpy.context
27+
import rclpy.executors
28+
import std_msgs.msg
29+
30+
31+
def generate_test_description(ready_fn):
32+
# Necessary to get real-time stdout from python processes:
33+
proc_env = os.environ.copy()
34+
proc_env['PYTHONUNBUFFERED'] = '1'
35+
36+
# Normally, talker publishes on the 'chatter' topic and listener listens on the
37+
# 'chatter' topic, but we want to show how to use remappings to munge the data so we
38+
# will remap these topics when we launch the nodes and insert our own node that can
39+
# change the data as it passes through
40+
talker_node = launch_ros.actions.Node(
41+
package='demo_nodes_py',
42+
node_executable='talker',
43+
env=proc_env,
44+
remappings=[('chatter', 'talker_chatter')],
45+
)
46+
47+
listener_node = launch_ros.actions.Node(
48+
package='demo_nodes_py',
49+
node_executable='listener',
50+
env=proc_env,
51+
)
52+
53+
return (
54+
launch.LaunchDescription([
55+
talker_node,
56+
listener_node,
57+
# Start tests right away - no need to wait for anything
58+
launch.actions.OpaqueFunction(function=lambda context: ready_fn()),
59+
]),
60+
{
61+
'talker': talker_node,
62+
'listener': listener_node,
63+
}
64+
)
65+
66+
67+
class TestTalkerListenerLink(unittest.TestCase):
68+
69+
@classmethod
70+
def setUpClass(cls, proc_output, listener):
71+
cls.context = rclpy.context.Context()
72+
rclpy.init(context=cls.context)
73+
cls.node = rclpy.create_node('test_node', context=cls.context)
74+
75+
# The demo node listener has no synchronization to indicate when it's ready to start
76+
# receiving messages on the /chatter topic. This plumb_listener method will attempt
77+
# to publish for a few seconds until it sees output
78+
publisher = cls.node.create_publisher(
79+
std_msgs.msg.String,
80+
'chatter'
81+
)
82+
msg = std_msgs.msg.String()
83+
msg.data = 'test message {}'.format(uuid.uuid4())
84+
for _ in range(5):
85+
try:
86+
publisher.publish(msg)
87+
proc_output.assertWaitFor(
88+
msg=msg.data,
89+
process=listener,
90+
timeout=1.0
91+
)
92+
except AssertionError:
93+
continue
94+
except NoMatchingProcessException:
95+
continue
96+
else:
97+
return
98+
else:
99+
assert False, 'Failed to plumb chatter topic to listener process'
100+
101+
@classmethod
102+
def tearDownClass(cls):
103+
cls.node.destroy_node()
104+
105+
def spin_rclpy(self, timeout_sec):
106+
executor = rclpy.executors.SingleThreadedExecutor(context=self.context)
107+
executor.add_node(self.node)
108+
try:
109+
executor.spin_once(timeout_sec=timeout_sec)
110+
finally:
111+
executor.remove_node(self.node)
112+
113+
def test_talker_transmits(self, talker):
114+
# Expect the talker to publish strings on '/talker_chatter' and also write to stdout
115+
msgs_rx = []
116+
sub = self.node.create_subscription(
117+
std_msgs.msg.String,
118+
'talker_chatter',
119+
callback=lambda msg: msgs_rx.append(msg)
120+
)
121+
self.addCleanup(self.node.destroy_subscription, sub)
122+
123+
# Wait until the talker transmits two messages over the ROS topic
124+
end_time = time.time() + 10
125+
while time.time() < end_time:
126+
self.spin_rclpy(1.0)
127+
if len(msgs_rx) > 2:
128+
break
129+
130+
self.assertGreater(len(msgs_rx), 2)
131+
132+
# Make sure the talker also output the same data via stdout
133+
for txt in [msg.data for msg in msgs_rx]:
134+
self.proc_output.assertWaitFor(
135+
msg=txt,
136+
process=talker
137+
)
138+
139+
def test_listener_receives(self, listener):
140+
pub = self.node.create_publisher(
141+
std_msgs.msg.String,
142+
'chatter'
143+
)
144+
self.addCleanup(self.node.destroy_publisher, pub)
145+
146+
# Publish some unique messages on /chatter and verify that the listener gets them
147+
# and prints them
148+
for _ in range(5):
149+
msg = std_msgs.msg.String()
150+
msg.data = str(uuid.uuid4())
151+
152+
pub.publish(msg)
153+
self.proc_output.assertWaitFor(
154+
msg=msg.data,
155+
process=listener
156+
)
157+
158+
def test_fuzzy_data(self, listener):
159+
# This test shows how to insert a node in between the talker and the listener to
160+
# change the data. Here we're going to change 'Hello World' to 'Aloha World'
161+
def data_mangler(msg):
162+
msg.data = msg.data.replace('Hello', 'Aloha')
163+
return msg
164+
165+
republisher = apex_launchtest_ros.DataRepublisher(
166+
self.node,
167+
'talker_chatter',
168+
'chatter',
169+
std_msgs.msg.String,
170+
data_mangler
171+
)
172+
self.addCleanup(republisher.shutdown)
173+
174+
# Spin for a few seconds until we've republished some mangled messages
175+
end_time = time.time() + 10
176+
while time.time() < end_time:
177+
self.spin_rclpy(1.0)
178+
if republisher.get_num_republished() > 2:
179+
break
180+
181+
self.assertGreater(republisher.get_num_republished(), 2)
182+
183+
# Sanity check that we're changing 'Hello World'
184+
self.proc_output.assertWaitFor('Aloha World')
185+
186+
# Check for the actual messages we sent
187+
for msg in republisher.get_republished():
188+
self.proc_output.assertWaitFor(msg.data, listener)

apex_rostest/apex_launchtest_ros/package.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<test_depend>ament_flake8</test_depend>
1616
<test_depend>ament_pep257</test_depend>
1717
<test_depend>python3-pytest</test_depend>
18+
<test_depend>demo_nodes_py</test_depend>
19+
<test_depend>std_msgs</test_depend>
1820

1921
<export>
2022
<build_type>ament_python</build_type>

apex_rostest/apex_launchtest_ros/setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
# This will let coverage find files with 0% coverage (not hit by tests at all)
33
source = .
44
omit = setup.py
5+
6+
[flake8]
7+
# For the purpose of import-order style checker, consider apex_launchtest as a local package
8+
import-order-style = "apex_launchtest"

apex_rostest/apex_launchtest_ros/setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#!/usr/bin/env python
22

3-
from setuptools import setup
43
import glob
54

5+
from setuptools import setup
6+
67
package_name = 'apex_launchtest_ros'
78

89
setup(
@@ -21,6 +22,6 @@
2122
packages=[
2223
'apex_launchtest_ros',
2324
],
24-
tests_require=["pytest"],
25+
tests_require=['pytest'],
2526
zip_safe=True,
2627
)

0 commit comments

Comments
 (0)