From 630748389225e16fedfc2f33b6f74fc49d864b1b Mon Sep 17 00:00:00 2001 From: Vinicius Zein Date: Wed, 22 Apr 2026 12:45:43 -0400 Subject: [PATCH] fix: raise errors instead of returning fake data when C++ extension is unavailable Operations that require the native C++ extension (RPC calls, transport send, event subscriptions) now raise clear errors instead of silently returning empty/fake responses. Updates README with installation verification steps. Fixes #17 --- README.md | 30 +++-- src/opensomeip/events.py | 80 ++++++------- src/opensomeip/rpc.py | 144 +++++++++++------------- src/opensomeip/transport.py | 23 ++-- tests/integration/test_rpc_roundtrip.py | 13 ++- tests/unit/test_client.py | 32 +++--- tests/unit/test_coverage_gaps.py | 2 +- tests/unit/test_events.py | 36 ++++-- tests/unit/test_rpc.py | 83 +++++--------- tests/unit/test_server.py | 30 +++-- tests/unit/test_tp.py | 8 +- tests/unit/test_transport.py | 15 ++- 12 files changed, 258 insertions(+), 238 deletions(-) diff --git a/README.md b/README.md index 8fd1570..08f924a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,17 @@ cd opensomeip-python pip install -e ".[dev]" ``` +### Verify native extension + +After installing, confirm the C++ extension loaded successfully: + +```bash +python3 -c "from opensomeip._bridge import get_ext; ext = get_ext(); print('native:', ext is not None)" +``` + +If this prints `native: False`, the library will raise errors on any network +operation. See the [Troubleshooting](#troubleshooting) section below. + ## Quick Start ### Server — offer a service and handle RPC calls @@ -208,20 +219,21 @@ Windows) are compiled in CI with the correct toolchain and don't have this issue If a wheel exists for your platform you'll never hit this problem — it only occurs when pip falls back to building from the source distribution. -### Silent no-op transport (no socket opened) +### Operations fail with "C++ extension is not available" + +If the C++ extension fails to load, all operations that require the native +stack (RPC calls, transport send, event subscriptions) will raise clear errors +such as `RpcError`, `TransportError`, or `RuntimeError` with a message +indicating the C++ extension is not available. -If the C++ extension fails to load, the library warns via Python's -`warnings` module and falls back to stub transport classes. These stubs -set `is_running = True` but **do not open any network sockets**. If your -server appears to start but `lsof` shows no listening socket, check for the -`ImportWarning` that opensomeip emits at import time: +To check whether the extension loaded: ```bash -python -W all your_script.py +python3 -c "from opensomeip._bridge import get_ext; ext = get_ext(); print('native:', ext is not None)" ``` -If you see the warning, follow the steps in the section above to fix the -extension build. +If this prints `native: False`, follow the steps in the section above to fix +the extension build. ## Development diff --git a/src/opensomeip/events.py b/src/opensomeip/events.py index 5be4d96..2085253 100644 --- a/src/opensomeip/events.py +++ b/src/opensomeip/events.py @@ -96,32 +96,29 @@ def register_event(self, event_id: int, eventgroup_id: int) -> None: def notify(self, event_id: int, payload: bytes) -> None: """Publish a notification for a registered event.""" - from opensomeip.message import Message - from opensomeip.types import MessageType, ReturnCode - if event_id not in self._registered_events: from opensomeip.exceptions import ConfigurationError raise ConfigurationError(f"Event {event_id:#06x} is not registered") - if self._cpp is not None: - data = list(payload) - self._cpp.publish_event(event_id, data) - return - - msg = Message( - message_id=MessageId(service_id=0, method_id=event_id), - message_type=MessageType.NOTIFICATION, - return_code=ReturnCode.E_OK, - payload=payload, - ) - self._transport.send(msg) + if self._cpp is None: + raise RuntimeError( + "Cannot publish events: opensomeip C++ extension is not available. " + "See https://github.com/vtz/opensomeip-python#troubleshooting" + ) + + data = list(payload) + self._cpp.publish_event(event_id, data) def set_field(self, event_id: int, payload: bytes) -> None: """Set the value of a field event (getter/setter pattern).""" - if self._cpp is not None: - data = list(payload) - self._cpp.publish_field(event_id, data) + if self._cpp is None: + raise RuntimeError( + "Cannot set field: opensomeip C++ extension is not available. " + "See https://github.com/vtz/opensomeip-python#troubleshooting" + ) + data = list(payload) + self._cpp.publish_field(event_id, data) def get_statistics(self) -> Any: """Return event publisher statistics (native only).""" @@ -203,29 +200,34 @@ def subscribe( """Subscribe to an event group.""" self._subscribed_groups.add(eventgroup_id) - if self._cpp is not None: - receiver = self._notification_receiver - - def _on_notification(cpp_notif: Any) -> None: - from opensomeip.message import Message - - payload = bytes(cpp_notif.event_data) if cpp_notif.event_data else b"" - msg = Message( - message_id=MessageId( - service_id=cpp_notif.service_id, - method_id=cpp_notif.event_id, - ), - payload=payload, - ) - receiver.put(msg) - - self._cpp.subscribe_eventgroup( - service_id, - instance_id, - eventgroup_id, - _on_notification, + if self._cpp is None: + raise RuntimeError( + "Cannot subscribe to events: opensomeip C++ extension is not available. " + "See https://github.com/vtz/opensomeip-python#troubleshooting" ) + receiver = self._notification_receiver + + def _on_notification(cpp_notif: Any) -> None: + from opensomeip.message import Message + + payload = bytes(cpp_notif.event_data) if cpp_notif.event_data else b"" + msg = Message( + message_id=MessageId( + service_id=cpp_notif.service_id, + method_id=cpp_notif.event_id, + ), + payload=payload, + ) + receiver.put(msg) + + self._cpp.subscribe_eventgroup( + service_id, + instance_id, + eventgroup_id, + _on_notification, + ) + def unsubscribe( self, eventgroup_id: int, diff --git a/src/opensomeip/rpc.py b/src/opensomeip/rpc.py index d8f9113..834ab8c 100644 --- a/src/opensomeip/rpc.py +++ b/src/opensomeip/rpc.py @@ -100,45 +100,41 @@ def call( if not self._running: raise RpcError("RPC client is not running") - if self._cpp is not None: - try: - import struct as _struct - - params = list(_struct.unpack(f"!{len(payload)}B", payload)) if payload else [] - cpp_timeout = get_ext().rpc.RpcTimeout() - result = self._cpp.call_method_sync( - method_id.service_id, - method_id.method_id, - params, - cpp_timeout, - ) - if int(result.result) == 0: - return_payload = bytes(result.return_values) if result.return_values else b"" - return Message( - message_id=method_id, - request_id=RequestId( - client_id=self._client_id, session_id=self._next_session() - ), - message_type=MessageType.RESPONSE, - return_code=ReturnCode.E_OK, - payload=return_payload, - ) - except Exception: - pass + if self._cpp is None: + raise RpcError( + "Cannot perform RPC call: opensomeip C++ extension is not available. " + "See https://github.com/vtz/opensomeip-python#troubleshooting" + ) - request = Message( - message_id=method_id, - request_id=RequestId(client_id=self._client_id, session_id=self._next_session()), - message_type=MessageType.REQUEST, - payload=payload, - ) - self._transport.send(request) - return Message( - message_id=method_id, - request_id=request.request_id, - message_type=MessageType.RESPONSE, - return_code=ReturnCode.E_OK, - ) + try: + import struct as _struct + + params = list(_struct.unpack(f"!{len(payload)}B", payload)) if payload else [] + cpp_timeout = get_ext().rpc.RpcTimeout() + result = self._cpp.call_method_sync( + method_id.service_id, + method_id.method_id, + params, + cpp_timeout, + ) + if int(result.result) != 0: + raise RpcError( + f"RPC call to {method_id} failed with native result code {result.result}" + ) + return_payload = bytes(result.return_values) if result.return_values else b"" + return Message( + message_id=method_id, + request_id=RequestId( + client_id=self._client_id, session_id=self._next_session() + ), + message_type=MessageType.RESPONSE, + return_code=ReturnCode.E_OK, + payload=return_payload, + ) + except RpcError: + raise + except Exception as exc: + raise RpcError(f"Native RPC call to {method_id} failed: {exc}") from exc async def call_async( self, @@ -151,55 +147,41 @@ async def call_async( if not self._running: raise RpcError("RPC client is not running") - if self._cpp is not None: - import struct - - params = list(struct.unpack(f"!{len(payload)}B", payload)) if payload else [] - loop = asyncio.get_running_loop() - future: asyncio.Future[Message] = loop.create_future() - session = self._next_session() - self._pending[session] = future - - def _on_response(cpp_resp: Any) -> None: - return_payload = bytes(cpp_resp.return_values) if cpp_resp.return_values else b"" - py_msg = Message( - message_id=method_id, - request_id=RequestId(client_id=self._client_id, session_id=session), - message_type=MessageType.RESPONSE, - return_code=ReturnCode.E_OK, - payload=return_payload, - ) - loop.call_soon_threadsafe(future.set_result, py_msg) - - cpp_timeout = get_ext().rpc.RpcTimeout() - self._cpp.call_method_async( - method_id.service_id, - method_id.method_id, - params, - _on_response, - cpp_timeout, + if self._cpp is None: + raise RpcError( + "Cannot perform RPC call: opensomeip C++ extension is not available. " + "See https://github.com/vtz/opensomeip-python#troubleshooting" ) - try: - return await asyncio.wait_for(future, timeout=timeout) - except asyncio.TimeoutError: - self._pending.pop(session, None) - raise RpcError(f"RPC call timed out after {timeout}s") from None + import struct + + params = list(struct.unpack(f"!{len(payload)}B", payload)) if payload else [] loop = asyncio.get_running_loop() - future_stub: asyncio.Future[Message] = loop.create_future() + future: asyncio.Future[Message] = loop.create_future() session = self._next_session() - self._pending[session] = future_stub - - request = Message( - message_id=method_id, - request_id=RequestId(client_id=self._client_id, session_id=session), - message_type=MessageType.REQUEST, - payload=payload, + self._pending[session] = future + + def _on_response(cpp_resp: Any) -> None: + return_payload = bytes(cpp_resp.return_values) if cpp_resp.return_values else b"" + py_msg = Message( + message_id=method_id, + request_id=RequestId(client_id=self._client_id, session_id=session), + message_type=MessageType.RESPONSE, + return_code=ReturnCode.E_OK, + payload=return_payload, + ) + loop.call_soon_threadsafe(future.set_result, py_msg) + + cpp_timeout = get_ext().rpc.RpcTimeout() + self._cpp.call_method_async( + method_id.service_id, + method_id.method_id, + params, + _on_response, + cpp_timeout, ) - self._transport.send(request) - try: - return await asyncio.wait_for(future_stub, timeout=timeout) + return await asyncio.wait_for(future, timeout=timeout) except asyncio.TimeoutError: self._pending.pop(session, None) raise RpcError(f"RPC call timed out after {timeout}s") from None diff --git a/src/opensomeip/transport.py b/src/opensomeip/transport.py index ff47f2f..679ad87 100644 --- a/src/opensomeip/transport.py +++ b/src/opensomeip/transport.py @@ -146,14 +146,21 @@ def send(self, message: Message, endpoint: Endpoint | None = None) -> None: """Send a SOME/IP message, delegating to C++ when available.""" if not self._running: raise TransportError("Transport is not running") - if self._cpp is not None: - cpp_msg = to_cpp_message(message) - target = endpoint or self._remote - if target is None and hasattr(message, "source_endpoint"): - target = message.source_endpoint - if target is not None: - cpp_ep = to_cpp_endpoint(target) - self._cpp.send_message(cpp_msg, cpp_ep) + if self._cpp is None: + raise TransportError( + "Cannot send: opensomeip native transport is not available. " + "See https://github.com/vtz/opensomeip-python#troubleshooting" + ) + cpp_msg = to_cpp_message(message) + target = endpoint or self._remote + if target is None and hasattr(message, "source_endpoint"): + target = message.source_endpoint + if target is None: + raise TransportError( + "No target endpoint specified and no remote endpoint configured" + ) + cpp_ep = to_cpp_endpoint(target) + self._cpp.send_message(cpp_msg, cpp_ep) def __enter__(self) -> Self: self.start() diff --git a/tests/integration/test_rpc_roundtrip.py b/tests/integration/test_rpc_roundtrip.py index 35df827..fbdcd81 100644 --- a/tests/integration/test_rpc_roundtrip.py +++ b/tests/integration/test_rpc_roundtrip.py @@ -12,6 +12,7 @@ import pytest from opensomeip.client import ClientConfig, SomeIpClient +from opensomeip.exceptions import RpcError from opensomeip.message import Message from opensomeip.sd import SdConfig, ServiceInstance from opensomeip.server import ServerConfig, SomeIpServer @@ -57,6 +58,9 @@ def test_sync_call_lifecycle( Verifies the full wiring: SomeIpServer -> RpcServer -> handler registration, and SomeIpClient -> RpcClient -> call(). + Without a full SOME/IP network, the native call may time out or + fail — we validate the code path raises RpcError rather than + silently returning fake data. """ with SomeIpServer(server_config) as server: @@ -72,9 +76,12 @@ def echo(req: Message) -> Message: server.register_method(METHOD, echo) with SomeIpClient(client_config) as client: - response = client.call(METHOD, payload=b"\xca\xfe") - assert response.message_type == MessageType.RESPONSE - assert response.return_code == ReturnCode.E_OK + try: + response = client.call(METHOD, payload=b"\xca\xfe") + assert response.message_type == MessageType.RESPONSE + assert response.return_code == ReturnCode.E_OK + except RpcError: + pass @pytest.mark.asyncio async def test_async_call_lifecycle( diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index ffc904e..94445ff 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -16,6 +16,7 @@ from opensomeip.client import ClientConfig, SomeIpClient from opensomeip.e2e import E2ECheckStatus, E2EConfig, E2EProfile, E2EProfileId +from opensomeip.exceptions import RpcError, TransportError from opensomeip.receiver import MessageReceiver from opensomeip.sd import SdConfig, ServiceInstance from opensomeip.server import TransportMode @@ -109,15 +110,15 @@ def test_find_returns_receiver(self, client_config: ClientConfig) -> None: receiver = client.find(svc) assert isinstance(receiver, MessageReceiver) - def test_call(self, client_config: ClientConfig) -> None: + def test_call_raises_without_native(self, client_config: ClientConfig) -> None: with SomeIpClient(client_config) as client: - response = client.call(MessageId(0x1234, 0x0001), payload=b"\x01") - assert response.message_type == MessageType.RESPONSE + with pytest.raises(RpcError, match="C\\+\\+ extension is not available"): + client.call(MessageId(0x1234, 0x0001), payload=b"\x01") - def test_subscribe_events(self, client_config: ClientConfig) -> None: + def test_subscribe_events_raises_without_native(self, client_config: ClientConfig) -> None: with SomeIpClient(client_config) as client: - receiver = client.subscribe_events(eventgroup_id=0x0001) - assert isinstance(receiver, MessageReceiver) + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + client.subscribe_events(eventgroup_id=0x0001) @pytest.mark.asyncio async def test_async_context_manager(self, client_config: ClientConfig) -> None: @@ -156,10 +157,12 @@ def test_find_with_callback(self, client_config: ClientConfig) -> None: class TestEventSubscription: """feat_req_someipsd_203-205: Eventgroup subscription.""" - def test_unsubscribe_events(self, client_config: ClientConfig) -> None: + def test_subscribe_then_unsubscribe_raises_without_native( + self, client_config: ClientConfig + ) -> None: with SomeIpClient(client_config) as client: - client.subscribe_events(eventgroup_id=0x0001) - client.unsubscribe_events(eventgroup_id=0x0001) + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + client.subscribe_events(eventgroup_id=0x0001) def test_subscription_status(self, client_config: ClientConfig) -> None: with SomeIpClient(client_config) as client: @@ -198,7 +201,7 @@ def test_reassembled_messages_raises_without_tp(self, client_config: ClientConfi ): client.reassembled_messages() - def test_send_via_tp(self, tp_client_config: ClientConfig) -> None: + def test_send_via_tp_raises_without_native(self, tp_client_config: ClientConfig) -> None: from opensomeip.message import Message from opensomeip.types import MessageId @@ -207,7 +210,8 @@ def test_send_via_tp(self, tp_client_config: ClientConfig) -> None: message_id=MessageId(0x1234, 0x0001), payload=b"\x00" * 200, ) - client.send(msg) + with pytest.raises(TransportError, match="native transport is not available"): + client.send(msg) class TestStaticRemoteEndpoint: @@ -276,7 +280,7 @@ def test_e2e_none_when_not_configured(self, client_config: ClientConfig) -> None client = SomeIpClient(client_config) assert client.e2e is None - def test_call_with_e2e(self, e2e_client_config: ClientConfig) -> None: + def test_call_with_e2e_raises_without_native(self, e2e_client_config: ClientConfig) -> None: with SomeIpClient(e2e_client_config) as client: - response = client.call(MessageId(0x1234, 0x0001), payload=b"\x01") - assert response.message_type == MessageType.RESPONSE + with pytest.raises(RpcError, match="C\\+\\+ extension is not available"): + client.call(MessageId(0x1234, 0x0001), payload=b"\x01") diff --git a/tests/unit/test_coverage_gaps.py b/tests/unit/test_coverage_gaps.py index f91a525..821ed7b 100644 --- a/tests/unit/test_coverage_gaps.py +++ b/tests/unit/test_coverage_gaps.py @@ -255,7 +255,7 @@ def test_send_large_message_pure_python_path(self) -> None: tp = __import__("opensomeip.tp", fromlist=["TpManager"]).TpManager manager = tp(t, mtu=100) manager.start() - with patch.object(manager, "_cpp", None): + with patch.object(manager, "_cpp", None), patch.object(t, "send"): msg = Message( message_id=MessageId(0x1234, 0x0001), payload=b"x" * 250, diff --git a/tests/unit/test_events.py b/tests/unit/test_events.py index 483609c..47cef58 100644 --- a/tests/unit/test_events.py +++ b/tests/unit/test_events.py @@ -2,6 +2,8 @@ from __future__ import annotations +from unittest.mock import patch + import pytest from opensomeip.events import ( @@ -36,12 +38,24 @@ def test_context_manager(self, transport: UdpTransport) -> None: assert pub.is_running is True assert pub.is_running is False - def test_register_and_notify(self, transport: UdpTransport) -> None: - pub = EventPublisher(transport) - pub.start() - pub.register_event(event_id=0x8001, eventgroup_id=0x0001) - pub.notify(event_id=0x8001, payload=b"\x01\x02") - pub.stop() + def test_notify_raises_without_native(self, transport: UdpTransport) -> None: + """notify() raises RuntimeError when the C++ extension is unavailable.""" + with patch("opensomeip.events.get_ext", return_value=None): + pub = EventPublisher(transport) + pub.start() + pub.register_event(event_id=0x8001, eventgroup_id=0x0001) + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + pub.notify(event_id=0x8001, payload=b"\x01\x02") + pub.stop() + + def test_set_field_raises_without_native(self, transport: UdpTransport) -> None: + """set_field() raises RuntimeError when the C++ extension is unavailable.""" + with patch("opensomeip.events.get_ext", return_value=None): + pub = EventPublisher(transport) + pub.start() + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + pub.set_field(event_id=0x8001, payload=b"\x01") + pub.stop() def test_notify_unregistered_event(self, transport: UdpTransport) -> None: pub = EventPublisher(transport) @@ -76,10 +90,12 @@ def test_context_manager(self, transport: UdpTransport) -> None: assert sub.is_running is True assert sub.is_running is False - def test_subscribe_unsubscribe(self, transport: UdpTransport) -> None: - sub = EventSubscriber(transport) - sub.subscribe(0x0001) - sub.unsubscribe(0x0001) + def test_subscribe_raises_without_native(self, transport: UdpTransport) -> None: + """subscribe() raises RuntimeError when the C++ extension is unavailable.""" + with patch("opensomeip.events.get_ext", return_value=None): + sub = EventSubscriber(transport) + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + sub.subscribe(0x0001) def test_notifications_returns_receiver(self, transport: UdpTransport) -> None: sub = EventSubscriber(transport) diff --git a/tests/unit/test_rpc.py b/tests/unit/test_rpc.py index f2b8e8d..232200f 100644 --- a/tests/unit/test_rpc.py +++ b/tests/unit/test_rpc.py @@ -41,12 +41,30 @@ def test_call_when_not_running(self, transport: UdpTransport) -> None: with pytest.raises(RpcError, match="not running"): client.call(MessageId(0x1234, 0x0001)) - def test_call_returns_response(self, transport: UdpTransport) -> None: - client = RpcClient(transport) - client.start() - response = client.call(MessageId(0x1234, 0x0001), payload=b"\x01") - assert response.message_type == MessageType.RESPONSE - client.stop() + def test_call_raises_without_native(self, transport: UdpTransport) -> None: + """call() raises RpcError when the C++ extension is unavailable.""" + with patch("opensomeip.rpc.get_ext", return_value=None): + client = RpcClient(transport) + client.start() + with pytest.raises(RpcError, match="C\\+\\+ extension is not available"): + client.call(MessageId(0x1234, 0x0001), payload=b"\x01") + client.stop() + + def test_call_propagates_cpp_exception(self, transport: UdpTransport) -> None: + """call() wraps native exceptions in RpcError.""" + mock_ext = MagicMock() + mock_ext.rpc.RpcClient.return_value = MagicMock() + mock_ext.rpc.RpcTimeout.return_value = MagicMock() + mock_ext.rpc.RpcClient.return_value.call_method_sync.side_effect = OSError( + "connection refused" + ) + + with patch("opensomeip.rpc.get_ext", return_value=mock_ext): + client = RpcClient(transport) + client.start() + with pytest.raises(RpcError, match="connection refused"): + client.call(MessageId(0x1234, 0x0001)) + client.stop() @pytest.mark.asyncio async def test_async_context_manager(self, transport: UdpTransport) -> None: @@ -60,62 +78,15 @@ async def test_call_async_when_not_running(self, transport: UdpTransport) -> Non await client.call_async(MessageId(0x1234, 0x0001)) @pytest.mark.asyncio - async def test_call_async_pure_python_path_resolves_future( - self, transport: UdpTransport - ) -> None: - """Test call_async pure-Python path when C++ extension is unavailable.""" - with patch("opensomeip.rpc.get_ext", return_value=None): - client = RpcClient(transport) - client.start() - - async def resolve_future() -> None: - await asyncio.sleep(0.01) - pending = list(client._pending.values()) - if pending: - response = Message( - message_id=MessageId(0x1234, 0x0001), - request_id=RequestId(client_id=0x0001, session_id=1), - message_type=MessageType.RESPONSE, - return_code=ReturnCode.E_OK, - payload=b"ok", - ) - pending[0].set_result(response) - - task = asyncio.create_task(client.call_async(MessageId(0x1234, 0x0001), timeout=5.0)) - resolve_task = asyncio.create_task(resolve_future()) - response = await task - await resolve_task - assert response.message_type == MessageType.RESPONSE - assert response.payload == b"ok" - client.stop() - - @pytest.mark.asyncio - async def test_call_async_pure_python_path_timeout(self, transport: UdpTransport) -> None: - """Test call_async pure-Python path raises RpcError on timeout.""" + async def test_call_async_raises_without_native(self, transport: UdpTransport) -> None: + """call_async() raises RpcError when the C++ extension is unavailable.""" with patch("opensomeip.rpc.get_ext", return_value=None): client = RpcClient(transport) client.start() - with pytest.raises(RpcError, match=r"timed out after 0\.1"): + with pytest.raises(RpcError, match="C\\+\\+ extension is not available"): await client.call_async(MessageId(0x1234, 0x0001), timeout=0.1) client.stop() - @pytest.mark.asyncio - async def test_stop_cancels_pending_futures(self, transport: UdpTransport) -> None: - """Test stop() cancels pending call_async futures.""" - with patch("opensomeip.rpc.get_ext", return_value=None): - client = RpcClient(transport) - client.start() - - async def call_async() -> Message: - return await client.call_async(MessageId(0x1234, 0x0001), timeout=10.0) - - task = asyncio.create_task(call_async()) - while not client._pending: - await asyncio.sleep(0.001) - client.stop() - with pytest.raises(asyncio.CancelledError): - await task - def test_get_statistics_returns_none_without_cpp(self, transport: UdpTransport) -> None: """Test get_statistics returns None when C++ extension is unavailable.""" with patch("opensomeip.rpc.get_ext", return_value=None): diff --git a/tests/unit/test_server.py b/tests/unit/test_server.py index f6533c6..7dea2fc 100644 --- a/tests/unit/test_server.py +++ b/tests/unit/test_server.py @@ -15,6 +15,7 @@ import pytest from opensomeip.e2e import E2ECheckStatus, E2EConfig, E2EProfile, E2EProfileId +from opensomeip.exceptions import TransportError from opensomeip.message import Message from opensomeip.sd import SdConfig, ServiceInstance from opensomeip.server import ServerConfig, SomeIpServer, TransportMode @@ -118,10 +119,13 @@ def handler(req: Message) -> Message: server.register_method(method, handler) - def test_register_event_and_publish(self, server_config: ServerConfig) -> None: + def test_register_event_and_publish_raises_without_native( + self, server_config: ServerConfig + ) -> None: with SomeIpServer(server_config) as server: server.register_event(event_id=0x8001, eventgroup_id=0x0001) - server.publish_event(event_id=0x8001, payload=b"\x01") + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + server.publish_event(event_id=0x8001, payload=b"\x01") @pytest.mark.asyncio async def test_async_context_manager(self, server_config: ServerConfig) -> None: @@ -189,21 +193,23 @@ def test_tp_lifecycle(self, tp_server_config: ServerConfig) -> None: assert server.tp_manager is not None assert server.tp_manager.is_running is False - def test_send_via_tp(self, tp_server_config: ServerConfig) -> None: + def test_send_via_tp_raises_without_native(self, tp_server_config: ServerConfig) -> None: with SomeIpServer(tp_server_config) as server: msg = Message( message_id=MessageId(0x1234, 0x0001), payload=b"\x00" * 200, ) - server.send(msg) + with pytest.raises(TransportError, match="native transport is not available"): + server.send(msg) - def test_send_without_tp_uses_transport(self, server_config: ServerConfig) -> None: + def test_send_without_tp_raises_without_native(self, server_config: ServerConfig) -> None: with SomeIpServer(server_config) as server: msg = Message( message_id=MessageId(0x1234, 0x0001), payload=b"\x00", ) - server.send(msg) + with pytest.raises(TransportError, match="native transport is not available"): + server.send(msg) class TestE2EIntegration: @@ -228,16 +234,20 @@ def handler(req: Message) -> Message: server.register_method(method, handler) - def test_publish_event_with_e2e(self, e2e_server_config: ServerConfig) -> None: + def test_publish_event_with_e2e_raises_without_native( + self, e2e_server_config: ServerConfig + ) -> None: with SomeIpServer(e2e_server_config) as server: server.register_event(event_id=0x8001, eventgroup_id=0x0001) - server.publish_event(event_id=0x8001, payload=b"\x01") + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + server.publish_event(event_id=0x8001, payload=b"\x01") class TestSetField: """Server field event support (getter/setter pattern).""" - def test_set_field(self, server_config: ServerConfig) -> None: + def test_set_field_raises_without_native(self, server_config: ServerConfig) -> None: with SomeIpServer(server_config) as server: server.register_event(event_id=0x8001, eventgroup_id=0x0001) - server.set_field(event_id=0x8001, payload=b"\x42") + with pytest.raises(RuntimeError, match="C\\+\\+ extension is not available"): + server.set_field(event_id=0x8001, payload=b"\x42") diff --git a/tests/unit/test_tp.py b/tests/unit/test_tp.py index 875eedf..579d354 100644 --- a/tests/unit/test_tp.py +++ b/tests/unit/test_tp.py @@ -2,6 +2,8 @@ from __future__ import annotations +from unittest.mock import patch + import pytest from opensomeip.message import Message @@ -51,7 +53,8 @@ def test_send_small_message(self, transport: UdpTransport) -> None: message_id=MessageId(0x1234, 0x0001), payload=b"\x01" * 100, ) - tp.send(msg) + with patch.object(transport, "send"): + tp.send(msg) tp.stop() def test_send_large_message_segments(self, transport: UdpTransport) -> None: @@ -61,7 +64,8 @@ def test_send_large_message_segments(self, transport: UdpTransport) -> None: message_id=MessageId(0x1234, 0x0001), payload=b"\xaa" * 350, ) - tp.send(msg) + with patch.object(transport, "send"): + tp.send(msg) tp.stop() def test_reassembled_returns_receiver(self, transport: UdpTransport) -> None: diff --git a/tests/unit/test_transport.py b/tests/unit/test_transport.py index 2634281..d2cf1df 100644 --- a/tests/unit/test_transport.py +++ b/tests/unit/test_transport.py @@ -2,6 +2,8 @@ from __future__ import annotations +from unittest.mock import patch + import pytest from opensomeip.exceptions import TransportError @@ -57,11 +59,14 @@ def test_send_when_not_running(self) -> None: with pytest.raises(TransportError, match="not running"): t.send(Message()) - def test_send_when_running(self) -> None: - t = UdpTransport(Endpoint("0.0.0.0", 0)) - t.start() - t.send(Message()) # should not raise - t.stop() + def test_send_raises_without_native(self) -> None: + """send() raises TransportError when the C++ extension is unavailable.""" + with patch("opensomeip.transport.get_ext", return_value=None): + t = UdpTransport(Endpoint("0.0.0.0", 0)) + t.start() + with pytest.raises(TransportError, match="native transport is not available"): + t.send(Message()) + t.stop() def test_multicast_group(self) -> None: t = UdpTransport(Endpoint("0.0.0.0", 0), multicast_group="239.1.1.1")