Skip to content

Commit

Permalink
Better testing of calling actions
Browse files Browse the repository at this point in the history
  • Loading branch information
felixdivo committed May 13, 2024
1 parent a1c1dec commit cfb0048
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 51 deletions.
4 changes: 2 additions & 2 deletions ros2_easy_test/ros2_easy_test/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,10 @@ def send_action_goal_and_wait_for_result(
result = self.await_future(goal_handle.get_result_async(), timeout=timeout_get_result)

# Make sure the goal was reached successfully
assert goal_handle.status == GoalStatus.STATUS_SUCCEEDED
assert result.status == GoalStatus.STATUS_SUCCEEDED, f"Goal did not succeed: {result.status=}"

# Return the results to the test case
return feedbacks, result
return feedbacks, result.result

def destroy_node(self):
# Actions don't get destroyed automatically
Expand Down
10 changes: 8 additions & 2 deletions ros2_easy_test/tests/example_nodes/minimal_action_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Modified from original example in examples_rclpy_minimal_action_server.server as follows:
# 1. pass *args, **kwargs to __init__ and super().__init__.
# 2. Reduce sleep times.
# 3. Reject goal requests with order < 0.
# 4. Cleanup/modernization.
#
# Copyright 2019 Open Source Robotics Foundation, Inc.
#
Expand Down Expand Up @@ -45,8 +47,12 @@ def destroy(self):
def goal_callback(self, goal_request):
"""Accept or reject a client request to begin an action."""
# This server allows multiple goals in parallel
self.get_logger().info("Received goal request")
return GoalResponse.ACCEPT
if goal_request.order < 0:
self.get_logger().info("Rejecting goal request")
return GoalResponse.REJECT
else:
self.get_logger().info("Accepting goal request")
return GoalResponse.ACCEPT

def cancel_callback(self, goal_handle):
"""Accept or reject a client request to cancel an action."""
Expand Down
47 changes: 0 additions & 47 deletions ros2_easy_test/tests/test_actions.py

This file was deleted.

56 changes: 56 additions & 0 deletions ros2_easy_test/tests/test_env_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from unittest import TestCase

# Testing
from action_msgs.srv import CancelGoal
from example_interfaces.action import Fibonacci
from pytest import mark
from std_msgs.msg import Empty, String

Expand All @@ -14,6 +16,7 @@
from . import LAUNCH_FILES

# Module under test and interfaces
from .example_nodes.minimal_action_server import MinimalActionServer
from .example_nodes.well_behaved import EchoNode, Talker


Expand Down Expand Up @@ -97,6 +100,59 @@ def test_mailbox_clearing_no_topics(self, env: ROS2TestEnvironment) -> None:
def test_wrong_topic_type(self, env: ROS2TestEnvironment) -> None:
pass

@mark.xfail(
raises=Exception,
reason="specifiying a wrong/nonexistent action server name a common mistake and shall fail loudly",
strict=True,
)
@with_single_node(MinimalActionServer)
def test_calling_nonexistent_action(self, env: ROS2TestEnvironment) -> None:
# The following isn't even an action at all, but these are no other actions easily available
env.send_action_goal("/does_no_exist", String("Hello World"))

@mark.xfail(
raises=TimeoutError,
reason="specifiying a wrong action message type is a common mistake and shall fail loudly",
strict=True,
)
@with_single_node(MinimalActionServer)
def test_calling_action_wrong_type(self, env: ROS2TestEnvironment) -> None:
env.send_action_goal("/does_no_exist", Fibonacci.Goal(order=1), timeout_availability=0.1)

@mark.xfail(
raises=AssertionError,
reason="rejections should be recognized as such",
strict=True,
)
@with_single_node(MinimalActionServer)
def test_action_rejection(self, env: ROS2TestEnvironment) -> None:
env.send_action_goal_and_wait_for_result("fibonacci", Fibonacci.Goal(order=-3))

@with_single_node(MinimalActionServer)
def test_cancelling_an_action(self, env: ROS2TestEnvironment) -> None:
goal_handle, _ = env.send_action_goal("fibonacci", Fibonacci.Goal(order=10))

cancel_response: CancelGoal.Response = goal_handle.cancel_goal()
assert cancel_response.return_code in {
CancelGoal.Response.ERROR_NONE,
CancelGoal.Response.ERROR_GOAL_TERMINATED,
}

@with_single_node(MinimalActionServer)
def test_concurrent_actions(self, env: ROS2TestEnvironment) -> None:
# (1) Call async
goal_handle1, _ = env.send_action_goal("fibonacci", Fibonacci.Goal(order=10))

# (2) Call sync
_, result2 = env.send_action_goal_and_wait_for_result("fibonacci", Fibonacci.Goal(order=1))

# Check result of (1)
result1 = goal_handle1.get_result().result
assert result1 == Fibonacci.Result(sequence=[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55])

# Check result of (2)
assert result2 == Fibonacci.Result(sequence=[0, 1])


if __name__ == "__main__":
unittest.main()
31 changes: 31 additions & 0 deletions ros2_easy_test/tests/test_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from unittest import TestCase

# Other ROS2 interfaces
from example_interfaces.action import Fibonacci
from example_interfaces.srv import AddTwoInts

# Testing
Expand All @@ -20,6 +21,7 @@
from . import LAUNCH_FILES

# Module under test and interfaces
from .example_nodes.minimal_action_server import MinimalActionServer
from .example_nodes.well_behaved import AddTwoIntsServer, EchoNode, Talker


Expand Down Expand Up @@ -95,6 +97,27 @@ def test_multiple_messages(self, env: ROS2TestEnvironment, count: int = 5) -> No
expected = [String(data=f"Hi #{identifier}") for identifier in range(count)]
self.assertListEqual(all_messages, expected)

def test_calling_an_action(self, env: ROS2TestEnvironment) -> None:
feedbacks, result = env.send_action_goal_and_wait_for_result(
name="fibonacci", goal=Fibonacci.Goal(order=4)
)

assert len(feedbacks) == 3
assert feedbacks == [
Fibonacci.Feedback(sequence=[0, 1, 1]),
Fibonacci.Feedback(sequence=[0, 1, 1, 2]),
Fibonacci.Feedback(sequence=[0, 1, 1, 2, 3]),
]

assert result == Fibonacci.Result(sequence=[0, 1, 1, 2, 3])

# We call it again to test if resources are reused properly
feedbacks2, result2 = env.send_action_goal_and_wait_for_result(
name="fibonacci", goal=Fibonacci.Goal(order=3)
)
assert feedbacks2 == feedbacks[:2]
assert result2 == Fibonacci.Result(sequence=[0, 1, 1, 2])


class TestSingleNode(SharedTestCases, TestCase):
"""This test case uses the ``with_single_node`` decorator to set up the test environment."""
Expand Down Expand Up @@ -137,6 +160,10 @@ def test_multiple_messages_stress_test(self, env: ROS2TestEnvironment) -> None:
def test_assertion_raised(self, env: ROS2TestEnvironment) -> None:
self.fail("This should fail the test case")

@with_single_node(MinimalActionServer)
def test_calling_an_action(self, env: ROS2TestEnvironment) -> None:
super().test_calling_an_action(env)


class TestLaunchFile(SharedTestCases, TestCase):
"""This test case uses the ``with_launch_file`` decorator to set up the test environment."""
Expand Down Expand Up @@ -195,6 +222,10 @@ def test_multiple_messages_stress_test(self, env: ROS2TestEnvironment) -> None:
def test_assertion_raised(self, env: ROS2TestEnvironment) -> None:
self.fail("This should fail the test case")

@with_launch_file(LAUNCH_FILES / "fibonacci_action.yaml")
def test_calling_an_action(self, env: ROS2TestEnvironment) -> None:
super().test_calling_an_action(env)


if __name__ == "__main__":
unittest.main()

0 comments on commit cfb0048

Please sign in to comment.