diff --git a/src/rai_core/rai/communication/ros2/ros_async.py b/src/rai_core/rai/communication/ros2/ros_async.py index eec6b8754..f249738b4 100644 --- a/src/rai_core/rai/communication/ros2/ros_async.py +++ b/src/rai_core/rai/communication/ros2/ros_async.py @@ -26,8 +26,13 @@ def get_future_result( future: rclpy.task.Future, timeout_sec: float = 5.0 ) -> Any | None: """Replaces rclpy.spin_until_future_complete""" - if timeout_sec <= 0: - raise ValueError(f"timeout_sec must be positive, got {timeout_sec}") + if timeout_sec is True or timeout_sec is False or not isinstance( + timeout_sec, (int, float) + ): + raise ValueError("timeout_sec must be a positive number") + timeout_sec = float(timeout_sec) + if timeout_sec != timeout_sec or timeout_sec <= 0: + raise ValueError("timeout_sec must be positive") result = None event = threading.Event() diff --git a/tests/communication_offline/test_get_future_result_timeout_validate.py b/tests/communication_offline/test_get_future_result_timeout_validate.py new file mode 100644 index 000000000..7676be0d3 --- /dev/null +++ b/tests/communication_offline/test_get_future_result_timeout_validate.py @@ -0,0 +1,29 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from unittest.mock import MagicMock + +import pytest + +# Import module via path without ROS tooling: ros_async imports rclpy +rclpy = pytest.importorskip("rclpy") + +from rai.communication.ros2.ros_async import get_future_result # noqa: E402 + + +@pytest.mark.parametrize("timeout", [0, -1, True, False, "1", math.nan]) +def test_get_future_result_rejects_bad_timeout(timeout): + with pytest.raises(ValueError, match="timeout_sec"): + get_future_result(MagicMock(), timeout_sec=timeout)