Skip to content

Commit a81ec58

Browse files
MaxWaterhoutMax Waterhout
andauthored
156 writing tests for panther in simulation (#169)
End-to-end tests: Verifies Panther’s movement behavior in simulation when controlled via joystick inputs. Integration test to see if the panther starts correctly and if the controllers work. Common test logic previously written for the Franka robot has been refactored and moved into shared utility functions Added a shared conftest. --------- Signed-off-by: Max Waterhout <[email protected]> Co-authored-by: Max Waterhout <[email protected]>
1 parent 4fcccf1 commit a81ec58

File tree

17 files changed

+610
-425
lines changed

17 files changed

+610
-425
lines changed

conftest.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-FileCopyrightText: Alliander N. V.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""
6+
Global pytest fixtures for ROS 2 integration testing.
7+
"""
8+
9+
from typing import Iterator
10+
11+
import pytest
12+
import rclpy
13+
from rclpy.node import Node
14+
15+
16+
@pytest.fixture(scope="module")
17+
def test_node() -> Iterator[Node]:
18+
"""Fixture to create a singleton node for testing."""
19+
rclpy.init()
20+
node = Node("test_node")
21+
yield node
22+
node.destroy_node()
23+
rclpy.shutdown()
24+
25+
26+
@pytest.fixture(scope="module")
27+
def timeout(pytestconfig: pytest.Config) -> int:
28+
"""Fixture to get the timeout value from pytest config."""
29+
return int(pytestconfig.getini("timeout"))
30+
31+
32+
@pytest.fixture(scope="session")
33+
def finger_joint_fault_tolerance() -> float:
34+
"""Tolerance of testing finger joint movements."""
35+
return 0.025
36+
37+
38+
@pytest.fixture(scope="session")
39+
def joint_movement_tolerance() -> float:
40+
"""Tolerance of testing joint movements."""
41+
return 0.01

docs/content/panther.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ Now you should be able to place a *2D Goal Pose* in Rviz, somewhere on the map.
180180
Don't forget to reset the E-STOP when the Panther should drive:
181181

182182
```bash
183-
ros2 service call /hardware/e_stop_reset std_srvs/srv/Trigger {}
183+
ros2 service call /panther/hardware/e_stop_reset std_srvs/srv/Trigger {}
184184
```
185185

186186
:::

ros2_ws/src/rcdt_franka/launch/franka.launch.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from launch import LaunchContext, LaunchDescription
66
from launch.actions import OpaqueFunction
7-
from launch_ros.actions import Node, SetParameter
7+
from launch_ros.actions import SetParameter
88
from rcdt_utilities.launch_utils import SKIP, LaunchArgument, get_file_path
99
from rcdt_utilities.register import Register, RegisteredLaunchDescription
1010

@@ -76,7 +76,9 @@ def launch_setup(context: LaunchContext) -> None:
7676
get_file_path("rcdt_franka", ["launch"], "gripper_services.launch.py")
7777
)
7878

79-
manipulate_pose = Node(package="rcdt_utilities", executable="manipulate_pose.py")
79+
utilities = RegisteredLaunchDescription(
80+
get_file_path("rcdt_utilities", ["launch"], "utils.launch.py")
81+
)
8082

8183
return [
8284
SetParameter(name="use_sim_time", value=use_sim),
@@ -85,7 +87,7 @@ def launch_setup(context: LaunchContext) -> None:
8587
Register.group(gripper_services, context),
8688
Register.group(moveit, context),
8789
Register.group(joystick, context),
88-
Register.on_start(manipulate_pose, context),
90+
Register.group(utilities, context),
8991
Register.group(rviz, context) if use_rviz else SKIP,
9092
Register.group(realsense, context) if use_realsense else SKIP,
9193
]

ros2_ws/src/rcdt_franka/test/conftest.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,10 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
from typing import Iterator
65

76
import pytest
8-
import rclpy
97
from rcdt_utilities.launch_utils import get_file_path
108
from rcdt_utilities.register import RegisteredLaunchDescription
11-
from rclpy.node import Node
12-
13-
14-
@pytest.fixture(scope="session")
15-
def test_node() -> Iterator[Node]:
16-
"""Fixture to create a singleton node for testing."""
17-
rclpy.init()
18-
node = Node("test_node")
19-
yield node
20-
node.destroy_node()
21-
rclpy.shutdown()
229

2310

2411
@pytest.fixture(scope="module")
@@ -57,14 +44,3 @@ def gripper_services_launch() -> RegisteredLaunchDescription:
5744
return RegisteredLaunchDescription(
5845
get_file_path("rcdt_franka", ["launch"], "gripper_services.launch.py")
5946
)
60-
61-
62-
@pytest.fixture(scope="session")
63-
def finger_joint_fault_tolerance() -> float:
64-
return 0.025
65-
66-
67-
@pytest.fixture(scope="session")
68-
def joint_movement_tolerance() -> float:
69-
"""Tolerance of testing joint movements."""
70-
return 0.01

ros2_ws/src/rcdt_franka/test/end_to_end/base_franka.py

Lines changed: 35 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,90 +3,35 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55

6-
import time
7-
86
import pytest
9-
import rclpy
10-
from geometry_msgs.msg import PoseStamped
11-
from rcdt_utilities.launch_utils import assert_for_message, assert_for_node
12-
from rcdt_utilities.test_utils import wait_for_register
7+
from rcdt_utilities.geometry import Pose
8+
from rcdt_utilities.launch_utils import assert_for_message
9+
from rcdt_utilities.test_utils import (
10+
assert_joy_topic_switch,
11+
assert_movements_with_joy,
12+
wait_for_register,
13+
wait_until_reached_joint,
14+
)
1315
from rclpy.node import Node
14-
from rclpy.qos import QoSDurabilityPolicy, QoSHistoryPolicy, QoSProfile
1516
from sensor_msgs.msg import JointState, Joy
16-
from std_msgs.msg import String
17-
from utils import EndToEndUtils, call_express_pose_in_other_frame
1817

1918

20-
class FrankaFullTests(EndToEndUtils):
21-
def test_wait_for_register(self, pytestconfig: pytest.Config) -> None:
22-
wait_for_register(pytestconfig)
19+
class FrankaFullTests:
20+
def test_wait_for_register(self, timeout: int) -> None:
21+
wait_for_register(timeout=timeout)
2322

24-
def test_joint_states_published(self) -> None:
23+
def test_joint_states_published(self, timeout: int) -> None:
2524
"""Test that joint states are published. This is a basic test to check that the
2625
launch file is working and that the robot is publishing joint states."""
27-
assert_for_message(JointState, "franka/joint_states", 60)
28-
29-
def test_ready_to_start(self, test_node: Node) -> None:
30-
"""This test will ensure the tests are ready to start by waiting for the move_group and moveit_manager node.
31-
Also waits until the gripper_action_controller is active."""
32-
assert (
33-
assert_for_node(
34-
fully_qualified_node_name="franka/move_group",
35-
test_node=test_node,
36-
timeout=30,
37-
)
38-
is True
39-
)
40-
assert (
41-
assert_for_node(
42-
fully_qualified_node_name="franka/moveit_manager",
43-
test_node=test_node,
44-
timeout=30,
45-
)
46-
is True
47-
)
48-
assert (
49-
self.wait_until_active(
50-
node=test_node, controller_name="gripper_action_controller"
51-
)
52-
is True
53-
)
26+
assert_for_message(JointState, "franka/joint_states", timeout=timeout)
5427

55-
def test_switch_joy_to_franka_topic(self, test_node: Node) -> None:
28+
def test_switch_joy_to_franka_topic(self, test_node: Node, timeout: int) -> None:
5629
"""Test to see if the switch to Franka mode is correct."""
57-
latched_qos = QoSProfile(
58-
depth=1,
59-
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
60-
history=QoSHistoryPolicy.KEEP_LAST,
61-
)
62-
result = {}
63-
64-
def listener_callback(msg: String) -> None:
65-
result["state"] = msg.data
66-
67-
# Subscribe to the correct topic
68-
test_node.create_subscription(
69-
String,
70-
"/joy_topic_manager/state", # this matches ~/state in JoyTopicManager
71-
listener_callback,
72-
qos_profile=latched_qos,
73-
)
74-
75-
pub = test_node.create_publisher(Joy, "/joy", 10)
76-
msg = Joy()
77-
msg.buttons = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
78-
pub.publish(msg)
79-
timeout_sec = 10.0
80-
start_time = time.time()
81-
while (
82-
result.get("state") != "/franka/joy"
83-
and time.time() - start_time < timeout_sec
84-
):
85-
rclpy.spin_once(test_node, timeout_sec=0.1)
86-
time.sleep(0.1)
87-
88-
assert result.get("state") == "/franka/joy", (
89-
f"Unexpected state: {result.get('state')}"
30+
assert_joy_topic_switch(
31+
node=test_node,
32+
expected_topic="/franka/joy",
33+
button_config=[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
34+
timeout=timeout,
9035
)
9136

9237
@pytest.mark.parametrize(
@@ -102,6 +47,7 @@ def test_joy_gripper_node(
10247
expected_value: float,
10348
test_node: Node,
10449
finger_joint_fault_tolerance: float,
50+
timeout: int,
10551
) -> None:
10652
"""Test that the joy node is running and the gripper is moving.
10753
The first call is an initialization call.
@@ -114,13 +60,13 @@ def test_joy_gripper_node(
11460
msg = Joy()
11561
msg.buttons = buttons
11662
pub.publish(msg)
117-
rclpy.spin_once(test_node, timeout_sec=0.1)
11863

119-
reached_goal, joint_value = self.wait_until_reached_joint(
64+
reached_goal, joint_value = wait_until_reached_joint(
12065
namespace="franka",
12166
joint="fr3_finger_joint1",
12267
expected_value=expected_value,
12368
tolerance=finger_joint_fault_tolerance,
69+
timeout_sec=timeout,
12470
)
12571
assert reached_goal is True, (
12672
f"The joint did not reach the joint. Currently {joint_value}, expected {expected_value}"
@@ -139,34 +85,22 @@ def test_move_arm_with_joy(
13985
axes: list[float],
14086
direction: str,
14187
test_node: Node,
88+
timeout: int,
14289
movement_threshold: float = 0.01,
14390
) -> None:
14491
"""Tests the linear movements of the hand while controlling with the joystick.
14592
Assert if it moves above a certain movement_threshold."""
146-
pose = PoseStamped()
147-
pose.header.frame_id = "franka/fr3_hand"
148-
first_pose = call_express_pose_in_other_frame(
149-
node=test_node, pose=pose, target_frame="franka/fr3_link0"
150-
).pose.pose
151-
152-
pub = test_node.create_publisher(Joy, "/joy", 10)
153-
154-
msg = Joy()
155-
msg.axes = axes
156-
pub.publish(msg)
157-
rclpy.spin_once(test_node, timeout_sec=0.1)
158-
time.sleep(1) # small delay for the robot to move
159-
160-
pose = PoseStamped()
161-
pose.header.frame_id = "franka/fr3_hand"
162-
moved_pose = call_express_pose_in_other_frame(
163-
node=test_node, pose=pose, target_frame="franka/fr3_link0"
164-
).pose.pose
165-
166-
delta = getattr(moved_pose.position, direction) - getattr(
167-
first_pose.position, direction
168-
)
16993

170-
assert abs(delta) > movement_threshold, (
171-
f"{direction} position did not change after input. Δ = {delta:.4f}"
94+
def compare_fn(p1: Pose, p2: Pose) -> float:
95+
return getattr(p2.position, direction) - getattr(p1.position, direction)
96+
97+
assert_movements_with_joy(
98+
node=test_node,
99+
joy_axes=axes,
100+
compare_fn=compare_fn,
101+
threshold=movement_threshold,
102+
description=f"{direction} position",
103+
frame_base="franka/fr3_hand",
104+
frame_target="franka/fr3_link0",
105+
timeout=timeout,
172106
)

ros2_ws/src/rcdt_franka/test/end_to_end/test_franka.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,21 @@
77
import pytest
88
from end_to_end.base_franka import FrankaFullTests
99
from launch import LaunchDescription
10-
from launch.actions import IncludeLaunchDescription
1110
from rcdt_utilities.launch_utils import get_file_path
11+
from rcdt_utilities.register import Register, RegisteredLaunchDescription
1212

1313

1414
@launch_pytest.fixture(scope="class")
1515
def franka_launch() -> LaunchDescription:
16-
return LaunchDescription(
17-
[
18-
IncludeLaunchDescription(
19-
get_file_path("rcdt_franka", ["launch"], "franka.launch.py"),
20-
launch_arguments={
21-
"rviz": "False",
22-
"world": "empty_camera.sdf",
23-
"realsense": "False",
24-
}.items(),
25-
)
26-
]
16+
franka = RegisteredLaunchDescription(
17+
get_file_path("rcdt_franka", ["launch"], "franka.launch.py"),
18+
launch_arguments={
19+
"rviz": "False",
20+
"world": "empty_camera.sdf",
21+
"realsense": "False",
22+
},
2723
)
24+
return Register.connect_context([franka])
2825

2926

3027
@pytest.mark.launch(fixture=franka_launch)

ros2_ws/src/rcdt_franka/test/integration/test_franka_core.py

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from rcdt_utilities.test_utils import get_joint_position, wait_for_register
1212
from rclpy.node import Node
1313
from sensor_msgs.msg import JointState
14-
from utils import call_move_gripper_service, follow_joint_trajectory_goal
14+
from utils import follow_joint_trajectory_goal
1515

1616

1717
@launch_pytest.fixture(scope="module")
@@ -23,42 +23,31 @@ def franka_core_launch(
2323

2424

2525
@pytest.mark.launch(fixture=franka_core_launch)
26-
def test_wait_for_register(pytestconfig: pytest.Config) -> None:
27-
wait_for_register(pytestconfig)
26+
def test_wait_for_register(timeout: int) -> None:
27+
wait_for_register(timeout=timeout)
2828

2929

3030
@pytest.mark.launch(fixture=franka_core_launch)
31-
def test_joint_states_published() -> None:
32-
assert_for_message(JointState, "franka/joint_states", 60)
33-
34-
35-
@pytest.mark.launch(fixture=franka_core_launch)
36-
def test_can_move_fingers(test_node: Node, finger_joint_fault_tolerance: float) -> None:
37-
assert (
38-
call_move_gripper_service(
39-
test_node,
40-
width=0.04,
41-
action_name="/franka/gripper_action_controller/gripper_cmd",
42-
)
43-
is True
44-
)
45-
pos = get_joint_position(namespace="franka", joint="fr3_finger_joint1")
46-
assert pos == pytest.approx(0.04, abs=finger_joint_fault_tolerance), (
47-
f"Got gripper position of {pos}"
48-
)
31+
def test_joint_states_published(timeout: int) -> None:
32+
assert_for_message(JointState, "franka/joint_states", timeout=timeout)
4933

5034

5135
@pytest.mark.launch(fixture=franka_core_launch)
5236
def test_follow_joint_trajectory_goal(
53-
test_node: Node, joint_movement_tolerance: float
37+
test_node: Node, joint_movement_tolerance: float, timeout: int
5438
) -> None:
5539
follow_joint_trajectory_goal(
5640
test_node,
5741
positions=[0.0, -0.5, 0.5, 0.0, 0.0, 0.0, 0.0],
5842
controller="franka/fr3_arm_controller",
43+
timeout=timeout,
44+
)
45+
pos_joint2 = get_joint_position(
46+
namespace="franka", joint="fr3_joint2", timeout=timeout
47+
)
48+
pos_joint3 = get_joint_position(
49+
namespace="franka", joint="fr3_joint3", timeout=timeout
5950
)
60-
pos_joint2 = get_joint_position(namespace="franka", joint="fr3_joint2")
61-
pos_joint3 = get_joint_position(namespace="franka", joint="fr3_joint3")
6251

6352
assert pos_joint2 == pytest.approx(-0.5, abs=joint_movement_tolerance), (
6453
f"Got joint position {pos_joint2}"

0 commit comments

Comments
 (0)