diff --git a/package.xml b/package.xml index 2493e793..9ee92f21 100644 --- a/package.xml +++ b/package.xml @@ -31,4 +31,6 @@ rospy rostest std_msgs + + rosnode diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7d4293d8..4add4fdb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -62,6 +62,7 @@ add_rostest(test_python_server2.launch) add_rostest(test_python_server3.launch) add_rostest(test_python_simple_server.launch) add_rostest(test_exercise_simple_clients.launch) +add_rostest(test_simple_action_server_deadlock_python.launch) catkin_add_gtest(actionlib-destruction_guard_test destruction_guard_test.cpp) if(TARGET actionlib-destruction_guard_test) diff --git a/test/simple_action_server_deadlock_companion.py b/test/simple_action_server_deadlock_companion.py new file mode 100755 index 00000000..286b3ab5 --- /dev/null +++ b/test/simple_action_server_deadlock_companion.py @@ -0,0 +1,79 @@ +#! /usr/bin/env python +# +# Copyright (c) 2013, Miguel Sarabia +# Imperial College London +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Imperial College London nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + + +class Constants: + node = "simple_action_server_deadlock_companion" + topic = "deadlock" + max_action_duration = 3 + +import random + +import actionlib +from actionlib.msg import TestAction, TestGoal +from actionlib_msgs.msg import GoalStatus +import rospy + + +class DeadlockCompanion: + + def __init__(self): + # Seed random with fully resolved name of node and current time + random.seed(rospy.get_name() + str(rospy.Time.now().to_sec())) + + # Create actionlib client + self.action_client = actionlib.SimpleActionClient( + Constants.topic, + TestAction) + + def run(self): + while not rospy.is_shutdown(): + # Send dummy goal + self.action_client.send_goal(TestGoal()) + + # Wait for a random amount of time + action_duration = random.uniform(0, Constants.max_action_duration) + self.action_client.wait_for_result(rospy.Duration(action_duration)) + + state = self.action_client.get_state() + if state == GoalStatus.ACTIVE or state == GoalStatus.PENDING: + self.action_client.cancel_goal() + + +if __name__ == '__main__': + rospy.init_node(Constants.node) + try: + companion = DeadlockCompanion() + companion.run() + except (KeyboardInterrupt, SystemExit): + raise + except: + pass diff --git a/test/test_simple_action_server_deadlock.py b/test/test_simple_action_server_deadlock.py new file mode 100755 index 00000000..ef0fc8c3 --- /dev/null +++ b/test/test_simple_action_server_deadlock.py @@ -0,0 +1,125 @@ +#! /usr/bin/env python +# +# Copyright (c) 2013, Miguel Sarabia +# Imperial College London +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Imperial College London nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + + +class Constants: + pkg = "actionlib" + node = "test_simple_action_server_deadlock" + topic = "deadlock" + deadlock_timeout = 45 # in seconds + shutdown_timeout = 2 # in seconds + max_action_duration = 3 + +import random +import sys +import threading +import unittest + +import actionlib +from actionlib.msg import TestAction +import rosnode +import rospy + + +class DeadlockTest(unittest.TestCase): + + def test_deadlock(self): + # Prepare condition (for safe preemption) + self.condition = threading.Condition() + self.last_execution_time = None + + # Prepare Simple Action Server + self.action_server = actionlib.SimpleActionServer( + Constants.topic, + TestAction, + execute_cb=self.execute_callback, + auto_start=False) + + self.action_server.register_preempt_callback(self.preempt_callback) + self.action_server.start() + + # Sleep for the amount specified + rospy.sleep(Constants.deadlock_timeout) + + # Start actual tests + running_nodes = set(rosnode.get_node_names()) + required_nodes = { + "/deadlock_companion_1", + "/deadlock_companion_2", + "/deadlock_companion_3", + "/deadlock_companion_4", + "/deadlock_companion_5"} + + self.assertTrue(required_nodes.issubset(running_nodes), + "Required companion nodes are not currently running") + + # Shutdown companions so that we can exit nicely + termination_time = rospy.Time.now() + rosnode.kill_nodes(required_nodes) + + rospy.sleep(Constants.shutdown_timeout) + + # Check last execution wasn't too long ago... + self.assertIsNotNone(self.last_execution_time is None, + "Execute Callback was never executed") + + time_since_last_execution = ( + termination_time - self.last_execution_time).to_sec() + + self.assertTrue( + time_since_last_execution < 2 * Constants.max_action_duration, + "Too long since last goal was executed; likely due to a deadlock") + + def execute_callback(self, goal): + # Note down last_execution time + self.last_execution_time = rospy.Time.now() + + # Determine duration of this action + action_duration = random.uniform(0, Constants.max_action_duration) + + with self.condition: + if not self.action_server.is_preempt_requested(): + self.condition.wait(action_duration) + + if self.action_server.is_preempt_requested(): + self.action_server.set_preempted() + else: + self.action_server.set_succeeded() + + def preempt_callback(self): + with self.condition: + self.condition.notify() + + +if __name__ == '__main__': + import rostest + rospy.init_node(Constants.node) + rostest.rosrun(Constants.pkg, Constants.node, DeadlockTest) diff --git a/test/test_simple_action_server_deadlock_python.launch b/test/test_simple_action_server_deadlock_python.launch new file mode 100644 index 00000000..f5940d1b --- /dev/null +++ b/test/test_simple_action_server_deadlock_python.launch @@ -0,0 +1,11 @@ + + + + + + + + + + +