From 5444d9b7e801af061dd479044a2e0e01b496e0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C5=82a=C5=BCej=20Sowa?= Date: Wed, 25 Dec 2024 15:26:46 +0100 Subject: [PATCH] Add ament_mypy test and fix all mypy errors (#980) --- rosapi/CMakeLists.txt | 3 ++ rosapi/package.xml | 1 + rosbridge_library/CMakeLists.txt | 3 ++ rosbridge_library/package.xml | 1 + .../capabilities/advertise_action.py | 12 +++---- .../capabilities/defragmentation.py | 2 +- .../capabilities/send_action_goal.py | 5 +-- .../capabilities/subscribe.py | 4 +-- .../src/rosbridge_library/internal/actions.py | 16 ++++----- .../rosbridge_library/internal/ros_loader.py | 10 +++--- .../rosbridge_library/internal/services.py | 16 ++++----- .../src/rosbridge_library/protocol.py | 5 +-- .../src/rosbridge_library/util/__init__.py | 4 +-- ...test_non-ros_service_client_complex-srv.py | 16 ++++----- .../test_non-ros_service_client_fragmented.py | 19 ++++++----- rosbridge_server/CMakeLists.txt | 3 ++ rosbridge_server/package.xml | 1 + .../test/websocket/advertise_action.test.py | 7 ++++ .../advertise_action_feedback.test.py | 8 ++++- .../test/websocket/advertise_service.test.py | 1 + .../advertise_service_duplicate.test.py | 1 + .../websocket/best_effort_publisher.test.py | 1 + .../test/websocket/call_service.test.py | 1 + rosbridge_server/test/websocket/common.py | 34 +++++++++++-------- .../websocket/multiple_subscribers.test.py | 1 + .../multiple_subscribers_raw.test.py | 1 + .../test/websocket/send_action_goal.test.py | 1 + rosbridge_server/test/websocket/smoke.test.py | 1 + .../transient_local_publisher.test.py | 1 + 29 files changed, 111 insertions(+), 68 deletions(-) diff --git a/rosapi/CMakeLists.txt b/rosapi/CMakeLists.txt index c9d5f7b8d..923abe8d8 100644 --- a/rosapi/CMakeLists.txt +++ b/rosapi/CMakeLists.txt @@ -25,4 +25,7 @@ if(BUILD_TESTING) find_package(ament_cmake_pytest REQUIRED) ament_add_pytest_test(${PROJECT_NAME}_test_stringify_field_types test/test_stringify_field_types.py) ament_add_pytest_test(${PROJECT_NAME}_test_typedefs test/test_typedefs.py) + + find_package(ament_cmake_mypy REQUIRED) + ament_mypy() endif() diff --git a/rosapi/package.xml b/rosapi/package.xml index 3457970ca..f5317efdd 100644 --- a/rosapi/package.xml +++ b/rosapi/package.xml @@ -34,6 +34,7 @@ rosgraph --> + ament_cmake_mypy ament_cmake_pytest sensor_msgs shape_msgs diff --git a/rosbridge_library/CMakeLists.txt b/rosbridge_library/CMakeLists.txt index dce17cda7..7373b0c5a 100644 --- a/rosbridge_library/CMakeLists.txt +++ b/rosbridge_library/CMakeLists.txt @@ -14,4 +14,7 @@ if (BUILD_TESTING) find_package(ament_cmake_pytest REQUIRED) ament_add_pytest_test(test_capabilities "test/capabilities/") ament_add_pytest_test(test_internal "test/internal/") + + find_package(ament_cmake_mypy REQUIRED) + ament_mypy() endif() diff --git a/rosbridge_library/package.xml b/rosbridge_library/package.xml index 1b4f38d7b..a1991536b 100644 --- a/rosbridge_library/package.xml +++ b/rosbridge_library/package.xml @@ -31,6 +31,7 @@ rosbridge_test_msgs action_msgs + ament_cmake_mypy ament_cmake_pytest builtin_interfaces diagnostic_msgs diff --git a/rosbridge_library/src/rosbridge_library/capabilities/advertise_action.py b/rosbridge_library/src/rosbridge_library/capabilities/advertise_action.py index 9cff54d8a..02042537d 100644 --- a/rosbridge_library/src/rosbridge_library/capabilities/advertise_action.py +++ b/rosbridge_library/src/rosbridge_library/capabilities/advertise_action.py @@ -33,11 +33,11 @@ import fnmatch from typing import Any -import rclpy from action_msgs.msg import GoalStatus from rclpy.action import ActionServer from rclpy.action.server import CancelResponse, ServerGoalHandle from rclpy.callback_groups import ReentrantCallbackGroup +from rclpy.task import Future from rosbridge_library.capability import Capability from rosbridge_library.internal import message_conversion from rosbridge_library.internal.ros_loader import get_action_class @@ -51,9 +51,9 @@ class AdvertisedActionHandler: def __init__( self, action_name: str, action_type: str, protocol: Protocol, sleep_time: float = 0.001 ) -> None: - self.goal_futures = {} - self.goal_handles = {} - self.goal_statuses = {} + self.goal_futures: dict[str, Future] = {} + self.goal_handles: dict[str, Any] = {} + self.goal_statuses: dict[str, GoalStatus] = {} self.action_name = action_name self.action_type = action_type @@ -79,7 +79,7 @@ async def execute_callback(self, goal: Any) -> Any: # generate a unique ID goal_id = f"action_goal:{self.action_name}:{self.next_id()}" - def done_callback(fut: rclpy.task.Future) -> None: + def done_callback(fut: Future) -> None: if fut.cancelled(): goal.abort() self.protocol.log("info", f"Aborted goal {goal_id}") @@ -94,7 +94,7 @@ def done_callback(fut: rclpy.task.Future) -> None: else: goal.abort() - future = rclpy.task.Future() + future: Future = Future() future.add_done_callback(done_callback) self.goal_handles[goal_id] = goal self.goal_futures[goal_id] = future diff --git a/rosbridge_library/src/rosbridge_library/capabilities/defragmentation.py b/rosbridge_library/src/rosbridge_library/capabilities/defragmentation.py index 1757a1179..7809622a7 100644 --- a/rosbridge_library/src/rosbridge_library/capabilities/defragmentation.py +++ b/rosbridge_library/src/rosbridge_library/capabilities/defragmentation.py @@ -30,7 +30,7 @@ def spam(self): # } # }, # ... - lists = {} + lists: dict[str, dict] = {} def __init__(self): """Create singleton instance""" diff --git a/rosbridge_library/src/rosbridge_library/capabilities/send_action_goal.py b/rosbridge_library/src/rosbridge_library/capabilities/send_action_goal.py index 089d84562..db74333db 100644 --- a/rosbridge_library/src/rosbridge_library/capabilities/send_action_goal.py +++ b/rosbridge_library/src/rosbridge_library/capabilities/send_action_goal.py @@ -33,6 +33,7 @@ import fnmatch from functools import partial from threading import Thread +from typing import Any from action_msgs.msg import GoalStatus from rosbridge_library.capability import Capability @@ -52,7 +53,7 @@ class SendActionGoal(Capability): cancel_action_goal_msg_fields = [(True, "action", str)] actions_glob = None - client_handler_list = {} + client_handler_list: dict[str, ActionClientHandler] = {} def __init__(self, protocol: Protocol) -> None: # Call superclass constructor @@ -182,7 +183,7 @@ def _failure(self, cid: str, action: str, exc: Exception) -> None: outgoing_message["id"] = cid self.protocol.send(outgoing_message) - def _feedback(self, cid: str, action: str, message: dict) -> None: + def _feedback(self, cid: str, action: str, message: Any) -> None: outgoing_message = { "op": "action_feedback", "action": action, diff --git a/rosbridge_library/src/rosbridge_library/capabilities/subscribe.py b/rosbridge_library/src/rosbridge_library/capabilities/subscribe.py index c54e80094..0f523ab22 100644 --- a/rosbridge_library/src/rosbridge_library/capabilities/subscribe.py +++ b/rosbridge_library/src/rosbridge_library/capabilities/subscribe.py @@ -40,10 +40,10 @@ from rosbridge_library.internal.subscription_modifiers import MessageHandler try: - from ujson import dumps as encode_json + from ujson import dumps as encode_json # type: ignore[import-untyped] except ImportError: try: - from simplejson import dumps as encode_json + from simplejson import dumps as encode_json # type: ignore[import-untyped] except ImportError: from json import dumps as encode_json diff --git a/rosbridge_library/src/rosbridge_library/internal/actions.py b/rosbridge_library/src/rosbridge_library/internal/actions.py index efc9211cc..8e3a0273e 100644 --- a/rosbridge_library/src/rosbridge_library/internal/actions.py +++ b/rosbridge_library/src/rosbridge_library/internal/actions.py @@ -32,7 +32,7 @@ import time from threading import Thread -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Optional from rclpy.action import ActionClient from rclpy.expand_topic_name import expand_topic_name @@ -59,9 +59,9 @@ def __init__( action: str, action_type: str, args: dict, - success_callback: Callable[[str, str, int, bool, dict], None], - error_callback: Callable[[str, str, Exception], None], - feedback_callback: Callable[[str, str, dict], None], + success_callback: Callable[[dict], None], + error_callback: Callable[[Exception], None], + feedback_callback: Optional[Callable[[dict], None]], node_handle: Node, ) -> None: """ @@ -90,12 +90,11 @@ def __init__( self.error = error_callback self.feedback = feedback_callback self.node_handle = node_handle - self.send_goal_helper = None + self.send_goal_helper = SendGoal() def run(self) -> None: try: # Call the service and pass the result to the success handler - self.send_goal_helper = SendGoal() self.success( self.send_goal_helper.send_goal( self.node_handle, @@ -110,7 +109,7 @@ def run(self) -> None: self.error(e) -def args_to_action_goal_instance(action: str, inst: Any, args: Union[list, dict]) -> Any: +def args_to_action_goal_instance(action: str, inst: Any, args: list | dict | None) -> Any: """ " Populate an action goal instance with the provided args @@ -142,6 +141,7 @@ def get_result_cb(self, future: Future) -> None: def goal_response_cb(self, future: Future) -> None: self.goal_handle = future.result() + assert self.goal_handle is not None if not self.goal_handle.accepted: raise Exception("Action goal was rejected") result_future = self.goal_handle.get_result_async() @@ -156,7 +156,7 @@ def send_goal( action: str, action_type: str, args: Optional[dict] = None, - feedback_cb: Optional[Callable[[str, str, dict], None]] = None, + feedback_cb: Optional[Callable[[dict], None]] = None, ) -> dict: # Given the action name and type, fetch a request instance action_name = expand_topic_name(action, node_handle.get_name(), node_handle.get_namespace()) diff --git a/rosbridge_library/src/rosbridge_library/internal/ros_loader.py b/rosbridge_library/src/rosbridge_library/internal/ros_loader.py index c45abb896..d3efa1664 100644 --- a/rosbridge_library/src/rosbridge_library/internal/ros_loader.py +++ b/rosbridge_library/src/rosbridge_library/internal/ros_loader.py @@ -43,9 +43,9 @@ """ # Variable containing the loaded classes -_loaded_msgs = {} -_loaded_srvs = {} -_loaded_actions = {} +_loaded_msgs: dict[str, Any] = {} +_loaded_srvs: dict[str, Any] = {} +_loaded_actions: dict[str, Any] = {} _msgs_lock = Lock() _srvs_lock = Lock() _actions_lock = Lock() @@ -185,7 +185,7 @@ def _get_class(typestring: str, subname: str, cache: Dict[str, Any], lock: Lock) return cls -def _load_class(modname: str, subname: str, classname: str) -> None: +def _load_class(modname: str, subname: str, classname: str) -> Any: """Loads the manifest and imports the module that contains the specified type. @@ -220,7 +220,7 @@ def _splittype(typestring: str) -> Tuple[str, str]: raise InvalidTypeStringException(typestring) -def _add_to_cache(cache: Dict[str, Any], lock: Lock, key: str, value: any) -> None: +def _add_to_cache(cache: Dict[str, Any], lock: Lock, key: str, value: Any) -> None: lock.acquire() cache[key] = value lock.release() diff --git a/rosbridge_library/src/rosbridge_library/internal/services.py b/rosbridge_library/src/rosbridge_library/internal/services.py index 59a76da7d..547877ef8 100644 --- a/rosbridge_library/src/rosbridge_library/internal/services.py +++ b/rosbridge_library/src/rosbridge_library/internal/services.py @@ -58,8 +58,8 @@ def __init__( self, service: str, args: dict, - success_callback: Callable[[str, str, int, bool, Any], None], - error_callback: Callable[[str, str, Exception], None], + success_callback: Callable[[dict], None], + error_callback: Callable[[Exception], None], node_handle: Node, ) -> None: """Create a service caller for the specified service. Use start() @@ -94,7 +94,7 @@ def run(self) -> None: self.error(e) -def args_to_service_request_instance(service: str, inst: Any, args: dict) -> Any: +def args_to_service_request_instance(service: str, inst: Any, args: list | dict | None) -> Any: """Populate a service request instance with the provided args args can be a dictionary of values, or a list, or None @@ -122,14 +122,14 @@ def call_service( service = expand_topic_name(service, node_handle.get_name(), node_handle.get_namespace()) service_names_and_types = dict(node_handle.get_service_names_and_types()) - service_type = service_names_and_types.get(service) - if service_type is None: + service_types = service_names_and_types.get(service) + if service_types is None: raise InvalidServiceException(service) # service_type is a tuple of types at this point; only one type is supported. - if len(service_type) > 1: - node_handle.get_logger().warning(f"More than one service type detected: {service_type}") - service_type = service_type[0] + if len(service_types) > 1: + node_handle.get_logger().warning(f"More than one service type detected: {service_types}") + service_type = service_types[0] service_class = get_service_class(service_type) inst = get_service_request_instance(service_type) diff --git a/rosbridge_library/src/rosbridge_library/protocol.py b/rosbridge_library/src/rosbridge_library/protocol.py index 9d91076c9..9d6e54f61 100644 --- a/rosbridge_library/src/rosbridge_library/protocol.py +++ b/rosbridge_library/src/rosbridge_library/protocol.py @@ -31,6 +31,7 @@ # POSSIBILITY OF SUCH DAMAGE. import time +from typing import Any from rosbridge_library.capabilities.fragmentation import Fragmentation from rosbridge_library.util import bson, json @@ -81,9 +82,9 @@ class Protocol: # !! this might be related to (or even be avoided by using) throttle_rate !! delay_between_messages = 0 # global list of non-ros advertised services - external_service_list = {} + external_service_list: dict[str, Any] = {} # global list of non-ros advertised actions - external_action_list = {} + external_action_list: dict[str, Any] = {} # Use only BSON for the whole communication if the server has been started with bson_only_mode:=True bson_only_mode = False diff --git a/rosbridge_library/src/rosbridge_library/util/__init__.py b/rosbridge_library/src/rosbridge_library/util/__init__.py index 8b04567e1..359021ae0 100644 --- a/rosbridge_library/src/rosbridge_library/util/__init__.py +++ b/rosbridge_library/src/rosbridge_library/util/__init__.py @@ -1,9 +1,9 @@ # try to import json-lib: 1st try ujson, 2nd try simplejson, else import standard Python json try: - import ujson as json + import ujson as json # type: ignore[import-untyped] except ImportError: try: - import simplejson as json + import simplejson as json # type: ignore[import-untyped] except ImportError: import json # noqa: F401 diff --git a/rosbridge_library/test/experimental/complex_srv+tcp/test_non-ros_service_client_complex-srv.py b/rosbridge_library/test/experimental/complex_srv+tcp/test_non-ros_service_client_complex-srv.py index eefc39f27..24f39f3cb 100755 --- a/rosbridge_library/test/experimental/complex_srv+tcp/test_non-ros_service_client_complex-srv.py +++ b/rosbridge_library/test/experimental/complex_srv+tcp/test_non-ros_service_client_complex-srv.py @@ -67,13 +67,13 @@ def request_service(): try: incoming = sock.recv(max_msg_length) # receive service_response from rosbridge if buffer == "": - buffer = incoming + buffer = incoming.decode("utf-8") if incoming == "": print("closing socket") sock.close() break else: - buffer = buffer + incoming + buffer = buffer + incoming.decode("utf-8") # print "buffer-length:", len(buffer) try: # try to access service_request directly (not fragmented) data_object = json.loads(buffer) @@ -90,14 +90,14 @@ def request_service(): "}{" ) # split buffer into fragments and re-fill curly brackets result = [] - for fragment in result_string: - if fragment[0] != "{": - fragment = "{" + fragment - if fragment[len(fragment) - 1] != "}": - fragment = fragment + "}" + for fragment_str in result_string: + if fragment_str[0] != "{": + fragment_str = "{" + fragment_str + if fragment_str[len(fragment_str) - 1] != "}": + fragment_str = fragment_str + "}" try: result.append( - json.loads(fragment) + json.loads(fragment_str) ) # try to parse json from string, and append if successful except Exception: # print(e) diff --git a/rosbridge_library/test/experimental/fragmentation+srv+tcp/test_non-ros_service_client_fragmented.py b/rosbridge_library/test/experimental/fragmentation+srv+tcp/test_non-ros_service_client_fragmented.py index 368d6c04c..e80ec9621 100755 --- a/rosbridge_library/test/experimental/fragmentation+srv+tcp/test_non-ros_service_client_fragmented.py +++ b/rosbridge_library/test/experimental/fragmentation+srv+tcp/test_non-ros_service_client_fragmented.py @@ -1,5 +1,6 @@ #!/usr/bin/python import socket +from typing import Any from rosbridge_library.util import json @@ -63,13 +64,13 @@ def request_service(): try: incoming = sock.recv(max_msg_length) # receive service_response from rosbridge if buffer == "": - buffer = incoming + buffer = incoming.decode("utf-8") if incoming == "": print("closing socket") sock.close() break else: - buffer = buffer + incoming + buffer = buffer + incoming.decode("utf-8") # print "buffer-length:", len(buffer) try: # try to access service_request directly (not fragmented) data_object = json.loads(buffer) @@ -86,14 +87,14 @@ def request_service(): "}{" ) # split buffer into fragments and re-fill curly brackets result = [] - for fragment in result_string: - if fragment[0] != "{": - fragment = "{" + fragment - if fragment[len(fragment) - 1] != "}": - fragment = fragment + "}" + for fragment_str in result_string: + if fragment_str[0] != "{": + fragment_str = "{" + fragment_str + if fragment_str[len(fragment_str) - 1] != "}": + fragment_str = fragment_str + "}" try: result.append( - json.loads(fragment) + json.loads(fragment_str) ) # try to parse json from string, and append if successful except Exception: # print(e) @@ -105,7 +106,7 @@ def request_service(): announced = int(result[0]["total"]) if fragment_count == announced: # if all fragments received --> sort and defragment # sort fragments - sorted_result = [None] * fragment_count + sorted_result: list[Any] = [None] * fragment_count unsorted_result = [] for fragment in result: unsorted_result.append(fragment) diff --git a/rosbridge_server/CMakeLists.txt b/rosbridge_server/CMakeLists.txt index b979bc359..048f6cbd5 100644 --- a/rosbridge_server/CMakeLists.txt +++ b/rosbridge_server/CMakeLists.txt @@ -32,4 +32,7 @@ if(BUILD_TESTING) add_launch_test(test/websocket/transient_local_publisher.test.py) add_launch_test(test/websocket/best_effort_publisher.test.py) add_launch_test(test/websocket/multiple_subscribers_raw.test.py) + + find_package(ament_cmake_mypy REQUIRED) + ament_mypy() endif() diff --git a/rosbridge_server/package.xml b/rosbridge_server/package.xml index 4520a09b4..31bdd65c8 100644 --- a/rosbridge_server/package.xml +++ b/rosbridge_server/package.xml @@ -24,6 +24,7 @@ rosbridge_msgs rosapi + ament_cmake_mypy example_interfaces python3-autobahn launch diff --git a/rosbridge_server/test/websocket/advertise_action.test.py b/rosbridge_server/test/websocket/advertise_action.test.py index 5021aa635..84997a697 100644 --- a/rosbridge_server/test/websocket/advertise_action.test.py +++ b/rosbridge_server/test/websocket/advertise_action.test.py @@ -7,6 +7,7 @@ from example_interfaces.action import Fibonacci from rclpy.action import ActionClient from rclpy.node import Node +from rclpy.task import Future from twisted.python import log sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory @@ -20,6 +21,9 @@ class TestAdvertiseAction(unittest.TestCase): + goal1_result_future: Future | None + goal2_result_future: Future | None + def goal1_response_callback(self, future): goal_handle = future.result() if not goal_handle.accepted: @@ -48,6 +52,7 @@ async def test_two_concurrent_calls(self, node: Node, make_client): requests_future, ws_client.message_handler = expect_messages( 2, "WebSocket", node.get_logger() ) + assert node.executor is not None requests_future.add_done_callback(lambda _: node.executor.wake()) self.goal1_result_future = None @@ -90,8 +95,10 @@ async def test_two_concurrent_calls(self, node: Node, make_client): } ) + assert self.goal1_result_future is not None result1 = await self.goal1_result_future self.assertEqual(result1.result, Fibonacci.Result(sequence=[0, 1, 1, 2])) + assert self.goal2_result_future is not None result2 = await self.goal2_result_future self.assertEqual(result2.result, Fibonacci.Result(sequence=[0, 1, 1, 2, 3, 5])) diff --git a/rosbridge_server/test/websocket/advertise_action_feedback.test.py b/rosbridge_server/test/websocket/advertise_action_feedback.test.py index 69b2c6864..af3f983cf 100644 --- a/rosbridge_server/test/websocket/advertise_action_feedback.test.py +++ b/rosbridge_server/test/websocket/advertise_action_feedback.test.py @@ -7,6 +7,7 @@ from example_interfaces.action import Fibonacci from rclpy.action import ActionClient from rclpy.node import Node +from rclpy.task import Future from twisted.python import log sys.path.append(os.path.dirname(__file__)) # enable importing from common.py in this directory @@ -20,8 +21,11 @@ class TestActionFeedback(unittest.TestCase): - def goal_response_callback(self, future): + goal_result_future: Future | None + + def goal_response_callback(self, future: Future): goal_handle = future.result() + assert goal_handle is not None if not goal_handle.accepted: return self.goal_result_future = goal_handle.get_result_async() @@ -45,6 +49,7 @@ async def test_feedback(self, node: Node, make_client): requests_future, ws_client.message_handler = expect_messages( 1, "WebSocket", node.get_logger() ) + assert node.executor is not None requests_future.add_done_callback(lambda _: node.executor.wake()) self.goal_result_future = None @@ -81,6 +86,7 @@ async def test_feedback(self, node: Node, make_client): } ) + assert self.goal_result_future is not None result = await self.goal_result_future self.assertIsNotNone(self.latest_feedback) self.assertEqual(self.latest_feedback.feedback, Fibonacci.Feedback(sequence=[0, 1, 1, 2])) diff --git a/rosbridge_server/test/websocket/advertise_service.test.py b/rosbridge_server/test/websocket/advertise_service.test.py index 6097dc3f3..724ffdf73 100644 --- a/rosbridge_server/test/websocket/advertise_service.test.py +++ b/rosbridge_server/test/websocket/advertise_service.test.py @@ -34,6 +34,7 @@ async def test_two_concurrent_calls(self, node: Node, make_client): requests_future, ws_client.message_handler = expect_messages( 2, "WebSocket", node.get_logger() ) + assert node.executor is not None requests_future.add_done_callback(lambda _: node.executor.wake()) response1_future = client.call_async(SetBool.Request(data=True)) diff --git a/rosbridge_server/test/websocket/advertise_service_duplicate.test.py b/rosbridge_server/test/websocket/advertise_service_duplicate.test.py index 466e44350..08ba07797 100644 --- a/rosbridge_server/test/websocket/advertise_service_duplicate.test.py +++ b/rosbridge_server/test/websocket/advertise_service_duplicate.test.py @@ -33,6 +33,7 @@ async def test_double_advertise(self, node: Node, make_client): requests1_future, ws_client1.message_handler = expect_messages( 1, "WebSocket 1", node.get_logger() ) + assert node.executor is not None requests1_future.add_done_callback(lambda _: node.executor.wake()) client.call_async(SetBool.Request(data=True)) diff --git a/rosbridge_server/test/websocket/best_effort_publisher.test.py b/rosbridge_server/test/websocket/best_effort_publisher.test.py index 2c84e6907..f011b8853 100644 --- a/rosbridge_server/test/websocket/best_effort_publisher.test.py +++ b/rosbridge_server/test/websocket/best_effort_publisher.test.py @@ -44,6 +44,7 @@ async def test_best_effort_publisher(self, node: Node, make_client): ws1_completed_future, ws_client1.message_handler = expect_messages( 1, "WebSocket 1", node.get_logger() ) + assert node.executor is not None ws1_completed_future.add_done_callback(lambda _: node.executor.wake()) self.assertEqual( diff --git a/rosbridge_server/test/websocket/call_service.test.py b/rosbridge_server/test/websocket/call_service.test.py index c8e041819..b6e8948be 100644 --- a/rosbridge_server/test/websocket/call_service.test.py +++ b/rosbridge_server/test/websocket/call_service.test.py @@ -35,6 +35,7 @@ def service_cb(req, res): responses_future, ws_client.message_handler = expect_messages( 1, "WebSocket", node.get_logger() ) + assert node.executor is not None responses_future.add_done_callback(lambda _: node.executor.wake()) ws_client.sendJson( diff --git a/rosbridge_server/test/websocket/common.py b/rosbridge_server/test/websocket/common.py index 450534ae9..4cc2f7ec5 100644 --- a/rosbridge_server/test/websocket/common.py +++ b/rosbridge_server/test/websocket/common.py @@ -7,9 +7,11 @@ import rclpy import rclpy.task from autobahn.twisted.websocket import WebSocketClientFactory, WebSocketClientProtocol +from launch.launch_description import LaunchDescription from rcl_interfaces.srv import GetParameters from rclpy.executors import SingleThreadedExecutor from rclpy.node import Node +from rclpy.task import Future from twisted.internet import reactor from twisted.internet.endpoints import TCP4ClientEndpoint @@ -23,7 +25,7 @@ class TestClientProtocol(WebSocketClientProtocol): def __init__(self, *args, **kwargs): self.received = [] - self.connected_future = rclpy.task.Future() + self.connected_future = Future() self.message_handler = lambda _: None super().__init__(*args, **kwargs) @@ -60,19 +62,19 @@ def _generate_node(): try: from launch_testing.actions import ReadyToTest - def generate_test_description() -> launch.LaunchDescription: + def generate_test_description() -> LaunchDescription: """ Generate a launch description that runs the websocket server. Re-export this from a test file and use add_launch_test() to run the test. """ - return launch.LaunchDescription([_generate_node(), ReadyToTest()]) + return LaunchDescription([_generate_node(), ReadyToTest()]) except ImportError: - def generate_test_description(ready_fn) -> launch.LaunchDescription: + def generate_test_description(ready_fn) -> LaunchDescription: # type: ignore[misc] """ Generate a launch description that runs the websocket server. Re-export this from a test file and use add_launch_test() to run the test. """ - return launch.LaunchDescription( + return LaunchDescription( [_generate_node(), launch.actions.OpaqueFunction(function=lambda context: ready_fn())] ) @@ -86,6 +88,7 @@ async def get_server_port(node: Node) -> int: if not client.wait_for_service(5): raise RuntimeError("GetParameters service not available") port_param = await client.call_async(GetParameters.Request(names=["actual_port"])) + assert port_param is not None return port_param.values[0].integer_value finally: node.destroy_client(client) @@ -96,18 +99,21 @@ async def connect_to_server(node: Node) -> TestClientProtocol: factory = WebSocketClientFactory("ws://127.0.0.1:" + str(port)) factory.protocol = TestClientProtocol - future = rclpy.task.Future() - future.add_done_callback(lambda _: node.executor.wake()) + future: Future = Future() + executor = node.executor + assert executor is not None + future.add_done_callback(lambda _: executor.wake()) def connect(): TCP4ClientEndpoint(reactor, "127.0.0.1", port).connect(factory).addCallback( future.set_result ) - reactor.callFromThread(connect) + reactor.callFromThread(connect) # type: ignore[attr-defined] - protocol = await future - protocol.connected_future.add_done_callback(lambda _: node.executor.wake()) + protocol: TestClientProtocol | None = await future + assert protocol is not None + protocol.connected_future.add_done_callback(lambda _: executor.wake()) await protocol.connected_future # wait for onOpen before proceeding return protocol @@ -128,8 +134,8 @@ async def task(): future = executor.create_task(task) - reactor.callInThread(executor.spin_until_future_complete, future) - reactor.run(installSignalHandlers=False) + reactor.callInThread(executor.spin_until_future_complete, future) # type: ignore[attr-defined] + reactor.run(installSignalHandlers=False) # type: ignore[attr-defined] executor.remove_node(node) node.destroy_node() @@ -140,7 +146,7 @@ def sleep(node: Node, duration: float) -> Awaitable[None]: """ Async-compatible delay function based on a ROS timer. """ - future = rclpy.task.Future() + future: Future = Future() def callback(): future.set_result(None) @@ -169,7 +175,7 @@ def expect_messages(count: int, description: str, logger): Convenience function to create a Future and a message handler function which gathers results into a list and waits for the list to have the expected number of items. """ - future = rclpy.Future() + future: Future = Future() results = [] def handler(msg): diff --git a/rosbridge_server/test/websocket/multiple_subscribers.test.py b/rosbridge_server/test/websocket/multiple_subscribers.test.py index 58aaadb1b..f49080483 100644 --- a/rosbridge_server/test/websocket/multiple_subscribers.test.py +++ b/rosbridge_server/test/websocket/multiple_subscribers.test.py @@ -40,6 +40,7 @@ async def test_multiple_subscribers(self, node: Node, make_client): ws1_completed_future, ws_client1.message_handler = expect_messages( 1, "WebSocket 1", node.get_logger() ) + assert node.executor is not None ws1_completed_future.add_done_callback(lambda _: node.executor.wake()) ws2_completed_future, ws_client2.message_handler = expect_messages( 1, "WebSocket 2", node.get_logger() diff --git a/rosbridge_server/test/websocket/multiple_subscribers_raw.test.py b/rosbridge_server/test/websocket/multiple_subscribers_raw.test.py index a1f5c1f3e..ddfe653c5 100644 --- a/rosbridge_server/test/websocket/multiple_subscribers_raw.test.py +++ b/rosbridge_server/test/websocket/multiple_subscribers_raw.test.py @@ -33,6 +33,7 @@ async def test_multiple_subscribers(self, node: Node, make_client): ws1_completed_future, ws_client1.message_handler = expect_messages( 1, "WebSocket 1", node.get_logger() ) + assert node.executor is not None ws1_completed_future.add_done_callback(lambda _: node.executor.wake()) ws2_completed_future, ws_client2.message_handler = expect_messages( 1, "WebSocket 2", node.get_logger() diff --git a/rosbridge_server/test/websocket/send_action_goal.test.py b/rosbridge_server/test/websocket/send_action_goal.test.py index 1db33ab53..bd4b9b77e 100644 --- a/rosbridge_server/test/websocket/send_action_goal.test.py +++ b/rosbridge_server/test/websocket/send_action_goal.test.py @@ -56,6 +56,7 @@ async def test_one_call(self, node: Node, make_client): responses_future, ws_client.message_handler = expect_messages( 5, "WebSocket", node.get_logger() ) + assert node.executor is not None responses_future.add_done_callback(lambda _: node.executor.wake()) ws_client.sendJson( diff --git a/rosbridge_server/test/websocket/smoke.test.py b/rosbridge_server/test/websocket/smoke.test.py index 2447ed424..f8959fa4c 100644 --- a/rosbridge_server/test/websocket/smoke.test.py +++ b/rosbridge_server/test/websocket/smoke.test.py @@ -37,6 +37,7 @@ async def test_smoke(self, node: Node, make_client): ws_completed_future, ws_client.message_handler = expect_messages( NUM_MSGS, "WebSocket", node.get_logger() ) + assert node.executor is not None ws_completed_future.add_done_callback(lambda _: node.executor.wake()) sub_a = node.create_subscription(String, A_TOPIC, sub_handler, NUM_MSGS) diff --git a/rosbridge_server/test/websocket/transient_local_publisher.test.py b/rosbridge_server/test/websocket/transient_local_publisher.test.py index d492350fb..1ad4ae1a0 100644 --- a/rosbridge_server/test/websocket/transient_local_publisher.test.py +++ b/rosbridge_server/test/websocket/transient_local_publisher.test.py @@ -46,6 +46,7 @@ async def test_transient_local_publisher(self, node: Node, make_client): ws_completed_future, ws_client.message_handler = expect_messages( 1, "WebSocket " + str(num), node.get_logger() ) + assert node.executor is not None ws_completed_future.add_done_callback(lambda _: node.executor.wake()) self.assertEqual(