Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enforce correct message type is passed to various API #317

Merged
merged 2 commits into from
Apr 24, 2019
Merged
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
12 changes: 12 additions & 0 deletions rclpy/rclpy/action/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,13 @@ def send_goal(self, goal, **kwargs):
:type goal: action_type.Goal
:return: The result response.
:rtype: action_type.Result
:raises: TypeError if the type of the passed goal isn't an instance of
the Goal type of the provided action when the service was
constructed.
"""
if not isinstance(goal, self._action_type.Goal):
raise TypeError()

event = threading.Event()

def unblock(future):
Expand Down Expand Up @@ -386,7 +392,13 @@ def send_goal_async(self, goal, feedback_callback=None, goal_uuid=None):
:return: a Future instance to a goal handle that completes when the goal request
has been accepted or rejected.
:rtype: :class:`rclpy.task.Future` instance
:raises: TypeError if the type of the passed goal isn't an instance of
the Goal type of the provided action when the service was
constructed.
"""
if not isinstance(goal, self._action_type.Goal):
raise TypeError()

request = self._action_type.Impl.SendGoalService.Request()
request.goal_id = self._generate_random_uuid() if goal_uuid is None else goal_uuid
request.goal = goal
Expand Down
3 changes: 3 additions & 0 deletions rclpy/rclpy/action/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ def execute(self, execute_callback=None):
self._action_server.notify_execute(self, execute_callback)

def publish_feedback(self, feedback):
if not isinstance(feedback, self._action_server.action_type.Feedback):
raise TypeError()

with self._lock:
# Ignore for already destructed goal handles
if self._handle is None:
Expand Down
12 changes: 12 additions & 0 deletions rclpy/rclpy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,13 @@ def call(self, request: SrvTypeRequest) -> SrvTypeResponse:

:param request: The service request.
:return: The service response.
:raises: TypeError if the type of the passed request isn't an instance
of the Request type of the provided service when the client was
constructed.
"""
if not isinstance(request, self.srv_type.Request):
raise TypeError()

event = threading.Event()

def unblock(future):
Expand Down Expand Up @@ -116,7 +122,13 @@ def call_async(self, request: SrvTypeRequest) -> Future:

:param request: The service request.
:return: A future that completes when the request does.
:raises: TypeError if the type of the passed request isn't an instance
of the Request type of the provided service when the client was
constructed.
"""
if not isinstance(request, self.srv_type.Request):
raise TypeError()

sequence_number = _rclpy.rclpy_send_request(self.client_handle, request)
if sequence_number in self._pending_requests:
raise RuntimeError('Sequence (%r) conflicts with pending request' % sequence_number)
Expand Down
7 changes: 5 additions & 2 deletions rclpy/rclpy/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ def publish(self, msg: MsgType) -> None:
"""
Send a message to the topic for the publisher.

:param msg: The ROS message to publish. The message must be the same type as the type
provided when the publisher was constructed.
:param msg: The ROS message to publish.
:raises: TypeError if the type of the passed message isn't an instance
of the provided type when the publisher was constructed.
"""
if not isinstance(msg, self.msg_type):
raise TypeError()
_rclpy.rclpy_publish(self.publisher_handle, msg)
5 changes: 5 additions & 0 deletions rclpy/rclpy/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,10 @@ def send_response(self, response: SrvTypeResponse, header) -> None:

:param response: The service response.
:param header: Capsule pointing to the service header from the original request.
:raises: TypeError if the type of the passed response isn't an instance
of the Response type of the provided service when the service was
constructed.
"""
if not isinstance(response, self.srv_type.Response):
raise TypeError()
_rclpy.rclpy_send_response(self.service_handle, response, header)
10 changes: 10 additions & 0 deletions rclpy/test/action/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,16 @@ def test_get_result_async(self):
finally:
ac.destroy()

def test_different_type_raises(self):
ac = ActionClient(self.node, Fibonacci, 'fibonacci')
try:
with self.assertRaises(TypeError):
ac.send_goal('different goal type')
with self.assertRaises(TypeError):
ac.send_goal_async('different goal type')
finally:
ac.destroy()


if __name__ == '__main__':
unittest.main()
34 changes: 34 additions & 0 deletions rclpy/test/action/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,40 @@ def execute_with_feedback(goal_handle):
self.mock_action_client.feedback_msg.feedback.sequence.tolist())
action_server.destroy()

def test_different_feedback_type_raises(self):

def execute_with_feedback(goal_handle):
try:
goal_handle.publish_feedback('different feedback type')
except TypeError:
feedback = Fibonacci.Feedback()
feedback.sequence = [1, 1, 2, 3]
goal_handle.publish_feedback(feedback)
goal_handle.succeed()
return Fibonacci.Result()

action_server = ActionServer(
self.node,
Fibonacci,
'fibonacci',
execute_callback=execute_with_feedback,
)

try:
goal_msg = Fibonacci.Impl.SendGoalService.Request()
goal_msg.goal_id = UUID(uuid=list(uuid.uuid4().bytes))
goal_future = self.mock_action_client.send_goal(goal_msg)

rclpy.spin_until_future_complete(
self.node, goal_future, self.executor)

feedback_msg = self.mock_action_client.feedback_msg
self.assertIsNotNone(feedback_msg)
self.assertEqual(
[1, 1, 2, 3], feedback_msg.feedback.sequence.tolist())
finally:
action_server.destroy()


if __name__ == '__main__':
unittest.main()
19 changes: 19 additions & 0 deletions rclpy/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ def test_concurrent_calls_to_service(self):
self.node.destroy_client(cli)
self.node.destroy_service(srv)

def test_different_type_raises(self):
cli = self.node.create_client(GetParameters, 'get/parameters')
srv = self.node.create_service(
GetParameters, 'get/parameters',
lambda request, response: 'different response type')
try:
with self.assertRaises(TypeError):
cli.call('different request type')
with self.assertRaises(TypeError):
cli.call_async('different request type')
self.assertTrue(cli.wait_for_service(timeout_sec=20))
future = cli.call_async(GetParameters.Request())
executor = rclpy.executors.SingleThreadedExecutor(context=self.context)
with self.assertRaises(TypeError):
rclpy.spin_until_future_complete(self.node, future, executor=executor)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this traceback look like? I would have expected the exception to be swallowed here:

rclpy/rclpy/rclpy/task.py

Lines 212 to 213 in afc4cd4

except Exception as e:
self.set_exception(e)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Output of the test without the with self.assertRaises(TypeError)::

rclpy/__init__.py:204: in spin_until_future_complete
    executor.spin_until_future_complete(future, timeout_sec)
rclpy/executors.py:271: in spin_until_future_complete
    self.spin_once(timeout_sec=timeout_sec)
rclpy/executors.py:628: in spin_once
    raise handler.exception()
rclpy/task.py:206: in __call__
    self._handler.send(None)
rclpy/executors.py:382: in handler
    await call_coroutine(entity, arg)
rclpy/executors.py:337: in _execute_service
    srv.send_response(response, header)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <rclpy.service.Service object at 0x7f549dd3bc88>
response = 'different response type'
header = <capsule object "rmw_request_id_t" at 0x7f549dd3c4e0>

    def send_response(self, response: SrvTypeResponse, header) -> None:
        """
        Send a service response.
    
        :param response: The service response.
        :param header: Capsule pointing to the service header from the original request.
        :raises: TypeError if the type of the passed response isn't an instance
          of the Response type of the provided service when the service was
          constructed.
        """
        if not isinstance(response, self.srv_type.Response):
>           raise TypeError()
E           TypeError

rclpy/service.py:79: TypeError

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh I didn't know about

if handler.exception() is not None:
raise handler.exception()
That's a nice improvement

finally:
self.node.destroy_client(cli)
self.node.destroy_service(srv)


if __name__ == '__main__':
unittest.main()
5 changes: 5 additions & 0 deletions rclpy/test/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ def test_invalid_string_raises(self):
pub = self.node.create_publisher(Primitives, 'chatter')
with self.assertRaises(UnicodeEncodeError):
pub.publish(msg)

def test_different_type_raises(self):
pub = self.node.create_publisher(Primitives, 'chatter')
with self.assertRaises(TypeError):
pub.publish('different message type')