Skip to content

Commit bff41ef

Browse files
Copilotgonzalocasas
andcommitted
Add ROS2 action client tests and fix KeyError for missing status field
Co-authored-by: gonzalocasas <[email protected]>
1 parent 1b4abbc commit bff41ef

File tree

2 files changed

+158
-3
lines changed

2 files changed

+158
-3
lines changed

src/roslibpy/comm/comm.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,18 @@ def _handle_action_result(self, message):
163163
resultback, _ , errback = action_handlers
164164
del self._pending_action_requests[request_id]
165165

166-
LOGGER.debug("Received Action result with status: %s", message["status"])
167-
168-
results = {"status": ActionGoalStatus(message["status"]).name, "values": message["values"]}
166+
# Handle different message formats for status field
167+
# ROS2 rosbridge may send status at top level or inside values
168+
status = message.get("status")
169+
if status is None and "values" in message:
170+
status = message["values"].get("status")
171+
if status is None:
172+
# Default to UNKNOWN if status is not found
173+
status = ActionGoalStatus.UNKNOWN.value
174+
175+
LOGGER.debug("Received Action result with status: %s", status)
176+
177+
results = {"status": ActionGoalStatus(status).name, "values": message.get("values", {})}
169178

170179
if "result" in message and message["result"] is False:
171180
if errback:

tests/ros2/test_action_client.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for ROS2 ActionClient message handling."""
2+
from unittest.mock import Mock
3+
4+
from roslibpy.comm.comm import RosBridgeProtocol
5+
from roslibpy.core import ActionResult, ActionGoalStatus
6+
7+
8+
def test_action_result_with_status_at_top_level():
9+
"""Test handling of action results with status at the top level of the message."""
10+
protocol = RosBridgeProtocol()
11+
protocol.factory = Mock()
12+
protocol.send_message = Mock()
13+
14+
result_callback = Mock()
15+
feedback_callback = Mock()
16+
error_callback = Mock()
17+
18+
request_id = "send_action_goal:/test_action:1"
19+
protocol._pending_action_requests[request_id] = (result_callback, feedback_callback, error_callback)
20+
21+
# ROS2 rosbridge message format with status at top level
22+
message = {
23+
"op": "action_result",
24+
"action": "/test_action",
25+
"id": request_id,
26+
"status": 4, # SUCCEEDED
27+
"values": {
28+
"result": {"success": True, "message": "Action completed"}
29+
},
30+
"result": True
31+
}
32+
33+
protocol._handle_action_result(message)
34+
35+
assert result_callback.called
36+
result = result_callback.call_args[0][0]
37+
assert isinstance(result, ActionResult)
38+
assert result["status"] == ActionGoalStatus.SUCCEEDED.name
39+
assert result["values"] == message["values"]
40+
41+
42+
def test_action_result_with_status_in_values():
43+
"""Test handling of action results with status inside the values field.
44+
45+
This reproduces the KeyError issue reported in GitHub issue.
46+
"""
47+
protocol = RosBridgeProtocol()
48+
protocol.factory = Mock()
49+
protocol.send_message = Mock()
50+
51+
result_callback = Mock()
52+
feedback_callback = Mock()
53+
error_callback = Mock()
54+
55+
request_id = "send_action_goal:/test_action:2"
56+
protocol._pending_action_requests[request_id] = (result_callback, feedback_callback, error_callback)
57+
58+
# Alternative message format with status inside values
59+
message = {
60+
"op": "action_result",
61+
"action": "/test_action",
62+
"id": request_id,
63+
"values": {
64+
"status": 4, # SUCCEEDED - status is here instead
65+
"result": {"success": True, "message": "Action completed"}
66+
},
67+
"result": True
68+
}
69+
70+
# This should not raise KeyError
71+
protocol._handle_action_result(message)
72+
73+
assert result_callback.called
74+
result = result_callback.call_args[0][0]
75+
assert isinstance(result, ActionResult)
76+
assert result["status"] == ActionGoalStatus.SUCCEEDED.name
77+
78+
79+
def test_action_result_failure_with_status_at_top_level():
80+
"""Test handling of failed action results."""
81+
protocol = RosBridgeProtocol()
82+
protocol.factory = Mock()
83+
protocol.send_message = Mock()
84+
85+
result_callback = Mock()
86+
feedback_callback = Mock()
87+
error_callback = Mock()
88+
89+
request_id = "send_action_goal:/test_action:3"
90+
protocol._pending_action_requests[request_id] = (result_callback, feedback_callback, error_callback)
91+
92+
message = {
93+
"op": "action_result",
94+
"action": "/test_action",
95+
"id": request_id,
96+
"status": 6, # ABORTED
97+
"values": {
98+
"result": {"success": False, "message": "Action failed"}
99+
},
100+
"result": False
101+
}
102+
103+
protocol._handle_action_result(message)
104+
105+
assert error_callback.called
106+
result = error_callback.call_args[0][0]
107+
assert result["status"] == ActionGoalStatus.ABORTED.name
108+
assert result["values"] == message["values"]
109+
110+
111+
def test_action_result_without_status_field():
112+
"""Test handling of action results when status field is missing entirely.
113+
114+
This is a defensive test for cases where neither top-level nor values contain status.
115+
In such cases, we should use a default status.
116+
"""
117+
protocol = RosBridgeProtocol()
118+
protocol.factory = Mock()
119+
protocol.send_message = Mock()
120+
121+
result_callback = Mock()
122+
feedback_callback = Mock()
123+
error_callback = Mock()
124+
125+
request_id = "send_action_goal:/test_action:4"
126+
protocol._pending_action_requests[request_id] = (result_callback, feedback_callback, error_callback)
127+
128+
# Message without status anywhere
129+
message = {
130+
"op": "action_result",
131+
"action": "/test_action",
132+
"id": request_id,
133+
"values": {
134+
"result": {"success": True}
135+
},
136+
"result": True
137+
}
138+
139+
# Should not raise KeyError, should use default status
140+
protocol._handle_action_result(message)
141+
142+
assert result_callback.called
143+
result = result_callback.call_args[0][0]
144+
assert isinstance(result, ActionResult)
145+
# Should have some status value (likely UNKNOWN)
146+
assert "status" in result

0 commit comments

Comments
 (0)