From 520f5ad3f81b2d3b56d0d22c384380869c04d89d Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Wed, 22 Jul 2026 06:52:40 -0400 Subject: [PATCH 1/2] fix(ci): add license header + align zero-timeout test with #808 rejection - tests/communication/__init__.py: add Apache license header (check-license-lines) - test_ros2_async.py: zero timeout now expects ValueError, matching get_future_result rejecting non-positive timeout_sec (#808) --- tests/communication/__init__.py | 13 +++++++++++++ tests/communication/ros2/test_ros2_async.py | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/communication/__init__.py b/tests/communication/__init__.py index e69de29bb..97ceef6f0 100644 --- a/tests/communication/__init__.py +++ b/tests/communication/__init__.py @@ -0,0 +1,13 @@ +# Copyright (C) 2025 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. diff --git a/tests/communication/ros2/test_ros2_async.py b/tests/communication/ros2/test_ros2_async.py index 25ec290c3..bd50055c9 100644 --- a/tests/communication/ros2/test_ros2_async.py +++ b/tests/communication/ros2/test_ros2_async.py @@ -125,10 +125,10 @@ def cancel_future(): # Edge case timeout tests def test_get_future_result_zero_timeout(): - """Test with zero timeout.""" + """Test that a zero timeout is rejected as non-positive.""" future = Future() - result = get_future_result(future, timeout_sec=0.0) - assert result is None + with pytest.raises(ValueError, match="timeout_sec must be positive"): + get_future_result(future, timeout_sec=0.0) def test_get_future_result_very_short_timeout(): From dd5cb71ad5367a5961a35767e1ac146868ba3ced Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Thu, 23 Jul 2026 02:10:01 -0400 Subject: [PATCH 2/2] fix(s2s): reject non-positive sound device read duration Signed-off-by: Bartok9 --- src/rai_s2s/rai_s2s/positive_params.py | 35 +++++++++++++ src/rai_s2s/rai_s2s/sound_device/api.py | 3 ++ tests/s2s/test_sound_device_read_duration.py | 54 ++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 src/rai_s2s/rai_s2s/positive_params.py create mode 100644 tests/s2s/test_sound_device_read_duration.py diff --git a/src/rai_s2s/rai_s2s/positive_params.py b/src/rai_s2s/rai_s2s/positive_params.py new file mode 100644 index 000000000..958a2ff30 --- /dev/null +++ b/src/rai_s2s/rai_s2s/positive_params.py @@ -0,0 +1,35 @@ +# 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. + +"""Pure positive-parameter guards for rai_s2s (no ROS / audio device imports).""" + +from __future__ import annotations + + +def require_positive_number(value, *, name: str = "value") -> float: + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise TypeError( + f"{name} must be a positive number, got {type(value).__name__}" + ) + if value <= 0: + raise ValueError(f"{name} must be positive, got {value!r}") + return float(value) + + +def require_positive_int(value, *, name: str = "value") -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise TypeError(f"{name} must be a positive int, got {type(value).__name__}") + if value <= 0: + raise ValueError(f"{name} must be positive, got {value!r}") + return value diff --git a/src/rai_s2s/rai_s2s/sound_device/api.py b/src/rai_s2s/rai_s2s/sound_device/api.py index 570b1313d..47d465127 100644 --- a/src/rai_s2s/rai_s2s/sound_device/api.py +++ b/src/rai_s2s/rai_s2s/sound_device/api.py @@ -17,6 +17,8 @@ from typing import Any, Callable, Optional import numpy as np + +from rai_s2s.positive_params import require_positive_number import sounddevice as sd from numpy._typing import NDArray from pydub import AudioSegment @@ -182,6 +184,7 @@ def read(self, time: float, blocking: bool = False) -> AudioSegment: if not self.read_flag: raise SoundDeviceError(f"{self.device_name} does not support reading!") + time = require_positive_number(time, name="time") frames = int(time * self.sample_rate) recording = sd.rec( frames=frames, diff --git a/tests/s2s/test_sound_device_read_duration.py b/tests/s2s/test_sound_device_read_duration.py new file mode 100644 index 000000000..26df5d1da --- /dev/null +++ b/tests/s2s/test_sound_device_read_duration.py @@ -0,0 +1,54 @@ +# 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 importlib.util +from pathlib import Path + +import pytest + + +def _load(): + path = ( + Path(__file__).resolve().parents[2] + / "src" + / "rai_s2s" + / "rai_s2s" + / "positive_params.py" + ) + spec = importlib.util.spec_from_file_location("pos_params_offline", path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return_mod = mod + return return_mod + + +@pytest.mark.parametrize("bad", [0, -1, -0.5, 0.0]) +def test_read_duration_rejects_non_positive(bad): + m = _load() + with pytest.raises(ValueError, match="time must be positive"): + m.require_positive_number(bad, name="time") + + +@pytest.mark.parametrize("bad", [True, False, "1", None]) +def test_read_duration_rejects_non_number(bad): + m = _load() + with pytest.raises(TypeError, match="time must be a positive number"): + m.require_positive_number(bad, name="time") + + +def test_read_duration_accepts_positive(): + m = _load() + assert m.require_positive_number(1.0, name="time") == 1.0 + assert m.require_positive_number(0.25, name="time") == 0.25