From 0d40750c34abbec99acc2bcd3e08ef85f6cca641 Mon Sep 17 00:00:00 2001 From: Vinicius Zein Date: Mon, 5 Oct 2026 09:35:10 -0400 Subject: [PATCH] chore: update opensomeip submodule to v0.2.0 Adapt the bindings so the extension still builds against the v0.2.0 API, including split TP statistics, E2E offset, and the RPC and SD behavior changes. Co-authored-by: Cursor --- extern/opensomeip | 2 +- pyproject.toml | 2 +- src/_opensomeip/e2e_bind.cpp | 5 ++- src/_opensomeip/events_bind.cpp | 12 ++++++- src/_opensomeip/message_bind.cpp | 5 +++ src/_opensomeip/rpc_bind.cpp | 55 ++++++++++++++++++++++++++--- src/_opensomeip/sd_bind.cpp | 3 ++ src/_opensomeip/tp_bind.cpp | 49 ++++++++++++++++++++++---- src/_opensomeip/transport_bind.cpp | 56 +++++++++++++++++++++++++++--- src/_opensomeip/types_bind.cpp | 1 + src/opensomeip/events.py | 5 +++ src/opensomeip/rpc.py | 9 +++-- src/opensomeip/sd.py | 19 +++++----- src/opensomeip/tp.py | 25 +++++++++++-- tests/unit/test_coverage_gaps.py | 5 +-- 15 files changed, 218 insertions(+), 35 deletions(-) diff --git a/extern/opensomeip b/extern/opensomeip index c66051f..23ff711 160000 --- a/extern/opensomeip +++ b/extern/opensomeip @@ -1 +1 @@ -Subproject commit c66051facd4d01c5c8ccf8d70fc5bb7f903c10c9 +Subproject commit 23ff71157c6b7a6b040884f7b17470adea60c6d6 diff --git a/pyproject.toml b/pyproject.toml index b523647..eb18505 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,4 +145,4 @@ name = "Miscellaneous" showcontent = false [tool.opensomeip] -opensomeip-version = "0.1.0" +opensomeip-version = "0.2.0" diff --git a/src/_opensomeip/e2e_bind.cpp b/src/_opensomeip/e2e_bind.cpp index f1dfeb1..e6ce36f 100644 --- a/src/_opensomeip/e2e_bind.cpp +++ b/src/_opensomeip/e2e_bind.cpp @@ -45,7 +45,10 @@ void init_e2e(py::module_& m) { .def_readwrite("profile_id", &E2EConfig::profile_id) .def_readwrite("profile_name", &E2EConfig::profile_name) .def_readwrite("data_id", &E2EConfig::data_id) - .def_readwrite("offset", &E2EConfig::offset) + .def_readwrite("offset_bits", &E2EConfig::offset_bits) + .def_property_readonly_static( + "DEFAULT_OFFSET_BITS", + [](py::object /*cls*/) { return E2EConfig::DEFAULT_OFFSET_BITS; }) .def_readwrite("enable_crc", &E2EConfig::enable_crc) .def_readwrite("enable_counter", &E2EConfig::enable_counter) .def_readwrite("enable_freshness", &E2EConfig::enable_freshness) diff --git a/src/_opensomeip/events_bind.cpp b/src/_opensomeip/events_bind.cpp index acbe434..779b046 100644 --- a/src/_opensomeip/events_bind.cpp +++ b/src/_opensomeip/events_bind.cpp @@ -112,9 +112,19 @@ void init_events(py::module_& m) { py::call_guard()) .def("set_default_client_endpoint", &EventPublisher::set_default_client_endpoint, py::arg("address"), py::arg("port")) - .def("handle_subscription", &EventPublisher::handle_subscription, + .def("handle_subscription", + static_cast&)>( + &EventPublisher::handle_subscription), py::arg("eventgroup_id"), py::arg("client_id"), py::arg("filters") = std::vector{}) + .def("handle_subscription", + static_cast&)>( + &EventPublisher::handle_subscription), + py::arg("eventgroup_id"), py::arg("client_id"), + py::arg("ttl_seconds"), + py::arg("filters") = std::vector{}) .def("handle_unsubscription", &EventPublisher::handle_unsubscription, py::arg("eventgroup_id"), py::arg("client_id")) .def("get_registered_events", &EventPublisher::get_registered_events) diff --git a/src/_opensomeip/message_bind.cpp b/src/_opensomeip/message_bind.cpp index 17d117c..cc70ab4 100644 --- a/src/_opensomeip/message_bind.cpp +++ b/src/_opensomeip/message_bind.cpp @@ -52,6 +52,11 @@ void init_message(py::module_& m) { std::vector vec(sv.begin(), sv.end()); return msg.deserialize(vec); }, py::arg("data")) + .def("try_deserialize", [](Message& msg, py::bytes data) { + auto sv = static_cast(data); + std::vector vec(sv.begin(), sv.end()); + return msg.try_deserialize(vec); + }, py::arg("data")) // Validation .def("is_valid", &Message::is_valid) diff --git a/src/_opensomeip/rpc_bind.cpp b/src/_opensomeip/rpc_bind.cpp index abdafb7..8e29869 100644 --- a/src/_opensomeip/rpc_bind.cpp +++ b/src/_opensomeip/rpc_bind.cpp @@ -20,8 +20,13 @@ void init_rpc(py::module_& m) { .value("INVALID_PARAMETERS", RpcResult::INVALID_PARAMETERS) .value("METHOD_NOT_FOUND", RpcResult::METHOD_NOT_FOUND) .value("SERVICE_NOT_AVAILABLE", RpcResult::SERVICE_NOT_AVAILABLE) + .value("WRONG_INTERFACE_VERSION", RpcResult::WRONG_INTERFACE_VERSION) .value("INTERNAL_ERROR", RpcResult::INTERNAL_ERROR); + py::enum_(rpc, "MethodSemantics") + .value("REQUEST_RESPONSE", MethodSemantics::REQUEST_RESPONSE) + .value("FIRE_AND_FORGET", MethodSemantics::FIRE_AND_FORGET); + py::class_(rpc, "RpcTimeout") .def(py::init<>()) .def_readwrite("request_timeout", &RpcTimeout::request_timeout) @@ -65,18 +70,52 @@ void init_rpc(py::module_& m) { .def_readonly("timeout_calls", &RpcClient::Statistics::timeout_calls) .def_readonly("average_response_time", &RpcClient::Statistics::average_response_time); rpc_client - .def(py::init(), py::arg("client_id")) + .def(py::init(), + py::arg("client_id"), + py::arg("interface_version") = static_cast(0x01), + py::arg("local_bind") = someip::transport::Endpoint("0.0.0.0", 0)) .def("initialize", &RpcClient::initialize) .def("shutdown", &RpcClient::shutdown, py::call_guard()) - .def("call_method_sync", &RpcClient::call_method_sync, + .def("set_remote_endpoint", &RpcClient::set_remote_endpoint, py::arg("endpoint")) + .def("get_local_endpoint", &RpcClient::get_local_endpoint) + .def("call_method_sync", + static_cast( + &RpcClient::call_method_sync), py::arg("service_id"), py::arg("method_id"), py::arg("parameters"), py::arg("timeout") = RpcTimeout(), py::call_guard()) - .def("call_method_async", &RpcClient::call_method_async, + .def("call_method_sync", + static_cast( + &RpcClient::call_method_sync), + py::arg("service_id"), py::arg("method_id"), + py::arg("parameters"), py::arg("server_endpoint"), + py::arg("timeout") = RpcTimeout(), + py::call_guard()) + .def("call_method_async", + static_cast( + &RpcClient::call_method_async), py::arg("service_id"), py::arg("method_id"), py::arg("parameters"), py::arg("callback"), py::arg("timeout") = RpcTimeout(), py::call_guard()) + .def("call_method_async", + static_cast( + &RpcClient::call_method_async), + py::arg("service_id"), py::arg("method_id"), + py::arg("parameters"), py::arg("callback"), + py::arg("server_endpoint"), py::arg("timeout") = RpcTimeout(), + py::call_guard()) + .def("send_request_no_return", &RpcClient::send_request_no_return, + py::arg("service_id"), py::arg("method_id"), + py::arg("parameters"), py::arg("dest"), + py::call_guard()) .def("cancel_call", &RpcClient::cancel_call, py::arg("handle")) .def("is_ready", &RpcClient::is_ready) .def("get_statistics", &RpcClient::get_statistics); @@ -90,11 +129,17 @@ void init_rpc(py::module_& m) { .def_readonly("method_not_found_errors", &RpcServer::Statistics::method_not_found_errors) .def_readonly("average_processing_time", &RpcServer::Statistics::average_processing_time); rpc_server - .def(py::init(), py::arg("service_id")) + .def(py::init(), + py::arg("service_id"), + py::arg("interface_version") = static_cast(0x01), + py::arg("bind_endpoint") = someip::transport::Endpoint( + "127.0.0.1", SOMEIP_DEFAULT_RPC_PORT)) .def("initialize", &RpcServer::initialize) .def("shutdown", &RpcServer::shutdown, py::call_guard()) .def("register_method", &RpcServer::register_method, - py::arg("method_id"), py::arg("handler")) + py::arg("method_id"), py::arg("handler"), + py::arg("semantics") = MethodSemantics::REQUEST_RESPONSE) + .def("get_local_endpoint", &RpcServer::get_local_endpoint) .def("unregister_method", &RpcServer::unregister_method, py::arg("method_id")) .def("is_method_registered", &RpcServer::is_method_registered, py::arg("method_id")) .def("get_registered_methods", &RpcServer::get_registered_methods) diff --git a/src/_opensomeip/sd_bind.cpp b/src/_opensomeip/sd_bind.cpp index 9fb0bdc..aff077a 100644 --- a/src/_opensomeip/sd_bind.cpp +++ b/src/_opensomeip/sd_bind.cpp @@ -118,6 +118,9 @@ void init_sd(py::module_& m) { py::arg("service_id"), py::arg("instance_id"), py::arg("eventgroup_id")) .def("get_available_services", &SdClient::get_available_services, py::arg("service_id") = 0) + .def("get_eventgroup_subscription_state", + &SdClient::get_eventgroup_subscription_state, + py::arg("service_id"), py::arg("instance_id"), py::arg("eventgroup_id")) .def("is_ready", &SdClient::is_ready) .def("get_statistics", &SdClient::get_statistics); diff --git a/src/_opensomeip/tp_bind.cpp b/src/_opensomeip/tp_bind.cpp index 72bb1a2..98a04c3 100644 --- a/src/_opensomeip/tp_bind.cpp +++ b/src/_opensomeip/tp_bind.cpp @@ -58,13 +58,32 @@ void init_tp(py::module_& m) { .def_readwrite("segment_offset", &TpSegmentHeader::segment_offset) .def_readwrite("segment_length", &TpSegmentHeader::segment_length) .def_readwrite("sequence_number", &TpSegmentHeader::sequence_number) - .def_readwrite("message_type", &TpSegmentHeader::message_type); + .def_readwrite("message_type", &TpSegmentHeader::message_type) + .def_readwrite("service_id", &TpSegmentHeader::service_id) + .def_readwrite("method_id", &TpSegmentHeader::method_id) + .def_readwrite("client_id", &TpSegmentHeader::client_id) + .def_readwrite("session_id", &TpSegmentHeader::session_id) + .def_readwrite("protocol_version", &TpSegmentHeader::protocol_version) + .def_readwrite("interface_version", &TpSegmentHeader::interface_version); py::class_(tp, "TpSegment") .def(py::init<>()) .def_readwrite("header", &TpSegment::header) .def_readwrite("payload", &TpSegment::payload) - .def_readwrite("retransmit_count", &TpSegment::retransmit_count); + .def_readwrite("retransmit_count", &TpSegment::retransmit_count) + .def_readwrite("sender_ipv4", &TpSegment::sender_ipv4) + .def_readwrite("sender_port", &TpSegment::sender_port); + + py::class_(tp, "TpReassemblyKey") + .def(py::init<>()) + .def_readwrite("message_id", &TpReassemblyKey::message_id) + .def_readwrite("protocol_version", &TpReassemblyKey::protocol_version) + .def_readwrite("interface_version", &TpReassemblyKey::interface_version) + .def_readwrite("message_type", &TpReassemblyKey::message_type) + .def_readwrite("request_id", &TpReassemblyKey::request_id) + .def_readwrite("sender_ipv4", &TpReassemblyKey::sender_ipv4) + .def_readwrite("sender_port", &TpReassemblyKey::sender_port) + .def("__eq__", &TpReassemblyKey::operator==); py::class_(tp, "TpStatistics") .def(py::init<>()) @@ -84,10 +103,27 @@ void init_tp(py::module_& m) { py::class_(tp, "TpReassembler") .def(py::init(), py::arg("config") = TpConfig()) - .def("process_segment", &TpReassembler::process_segment, + .def("process_segment", + [](TpReassembler& reassembler, const TpSegment& segment, + someip::platform::ByteBuffer& complete_message) { + return reassembler.process_segment(segment, complete_message, nullptr); + }, py::arg("segment"), py::arg("complete_message")) - .def("is_reassembling", &TpReassembler::is_reassembling, py::arg("message_id")) - .def("cancel_reassembly", &TpReassembler::cancel_reassembly, py::arg("message_id")) + .def("is_reassembling", + static_cast( + &TpReassembler::is_reassembling), + py::arg("message_id")) + .def("is_reassembling", + static_cast( + &TpReassembler::is_reassembling), + py::arg("key")) + .def("cancel_reassembly", + static_cast(&TpReassembler::cancel_reassembly), + py::arg("message_id")) + .def("cancel_reassembly", + static_cast( + &TpReassembler::cancel_reassembly), + py::arg("key")) .def("process_timeouts", &TpReassembler::process_timeouts) .def("get_active_reassemblies", &TpReassembler::get_active_reassemblies) .def("update_config", &TpReassembler::update_config, py::arg("config")); @@ -118,6 +154,7 @@ void init_tp(py::module_& m) { .def("set_progress_callback", &TpManager::set_progress_callback, py::arg("callback")) .def("set_message_callback", &TpManager::set_message_callback, py::arg("callback")) .def("process_timeouts", &TpManager::process_timeouts) - .def("get_statistics", &TpManager::get_statistics) + .def("get_sender_statistics", &TpManager::get_sender_statistics) + .def("get_receiver_statistics", &TpManager::get_receiver_statistics) .def("update_config", &TpManager::update_config, py::arg("config")); } diff --git a/src/_opensomeip/transport_bind.cpp b/src/_opensomeip/transport_bind.cpp index a9d9830..a9c2eb9 100644 --- a/src/_opensomeip/transport_bind.cpp +++ b/src/_opensomeip/transport_bind.cpp @@ -3,6 +3,7 @@ #include #include "transport/endpoint.h" +#include "transport/message_rejection.h" #include "transport/transport.h" #include "transport/udp_transport.h" #include "transport/tcp_transport.h" @@ -33,6 +34,11 @@ struct PyTransportListener : ITransportListener { py::gil_scoped_acquire gil; PYBIND11_OVERRIDE_PURE(void, ITransportListener, on_error, error); } + + void on_message_rejected(const MessageRejectionInfo& info) override { + py::gil_scoped_acquire gil; + PYBIND11_OVERRIDE(void, ITransportListener, on_message_rejected, info); + } }; void init_transport(py::module_& m) { @@ -65,12 +71,31 @@ void init_transport(py::module_& m) { .value("CONNECTED", TcpConnectionState::CONNECTED) .value("DISCONNECTING", TcpConnectionState::DISCONNECTING); + py::enum_(m, "MessageRejectionStage") + .value("DESERIALIZE", MessageRejectionStage::DESERIALIZE) + .value("TCP_FRAMING", MessageRejectionStage::TCP_FRAMING) + .value("TP_REASSEMBLY", MessageRejectionStage::TP_REASSEMBLY) + .value("E2E_INTEGRITY", MessageRejectionStage::E2E_INTEGRITY); + + py::class_(m, "MessageRejectionInfo") + .def(py::init<>()) + .def_readwrite("sender", &MessageRejectionInfo::sender) + .def_readwrite("result", &MessageRejectionInfo::result) + .def_readwrite("stage", &MessageRejectionInfo::stage) + .def_readwrite("has_message_id", &MessageRejectionInfo::has_message_id) + .def_readwrite("message_id", &MessageRejectionInfo::message_id) + .def_readwrite("has_request_id", &MessageRejectionInfo::has_request_id) + .def_readwrite("request_id", &MessageRejectionInfo::request_id); + py::class_(m, "ITransportListener") .def(py::init<>()) - .def("on_message_received", &ITransportListener::on_message_received) + .def("on_message_received", + static_cast( + &ITransportListener::on_message_received)) .def("on_connection_lost", &ITransportListener::on_connection_lost) .def("on_connection_established", &ITransportListener::on_connection_established) - .def("on_error", &ITransportListener::on_error); + .def("on_error", &ITransportListener::on_error) + .def("on_message_rejected", &ITransportListener::on_message_rejected); py::class_(m, "UdpTransportConfig") .def(py::init<>()) @@ -82,7 +107,14 @@ void init_transport(py::module_& m) { .def_readwrite("enable_broadcast", &UdpTransportConfig::enable_broadcast) .def_readwrite("multicast_interface", &UdpTransportConfig::multicast_interface) .def_readwrite("multicast_ttl", &UdpTransportConfig::multicast_ttl) - .def_readwrite("max_message_size", &UdpTransportConfig::max_message_size); + .def_readwrite("max_message_size", &UdpTransportConfig::max_message_size) + .def_readwrite("enable_tp", &UdpTransportConfig::enable_tp); + + py::class_(m, "MulticastError") + .def(py::init<>()) + .def_readwrite("group_address", &MulticastError::group_address) + .def_readwrite("interface_address", &MulticastError::interface_address) + .def_readwrite("system_error", &MulticastError::system_error); py::class_(m, "UdpTransport") .def(py::init(), @@ -94,6 +126,11 @@ void init_transport(py::module_& m) { py::arg("message"), py::arg("endpoint"), py::call_guard()) .def("receive_message", &UdpTransport::receive_message) + .def("receive_message_with_sender", [](UdpTransport& transport) { + Endpoint sender; + MessagePtr message = transport.receive_message_with_sender(sender); + return py::make_tuple(std::move(message), sender); + }) .def("is_running", &UdpTransport::is_running) .def("is_connected", &UdpTransport::is_connected) .def("get_local_endpoint", &UdpTransport::get_local_endpoint) @@ -104,7 +141,8 @@ void init_transport(py::module_& m) { py::call_guard()) .def("leave_multicast_group", &UdpTransport::leave_multicast_group, py::arg("multicast_address"), - py::call_guard()); + py::call_guard()) + .def("last_multicast_error", &UdpTransport::last_multicast_error); py::class_(m, "TcpTransportConfig") .def(py::init<>()) @@ -133,6 +171,11 @@ void init_transport(py::module_& m) { py::arg("message"), py::arg("endpoint"), py::call_guard()) .def("receive_message", &TcpTransport::receive_message) + .def("receive_message_with_sender", [](TcpTransport& transport) { + Endpoint sender; + MessagePtr message = transport.receive_message_with_sender(sender); + return py::make_tuple(std::move(message), sender); + }) .def("is_running", &TcpTransport::is_running) .def("is_connected", &TcpTransport::is_connected) .def("get_local_endpoint", &TcpTransport::get_local_endpoint) @@ -142,7 +185,10 @@ void init_transport(py::module_& m) { .def("enable_server_mode", &TcpTransport::enable_server_mode, py::arg("backlog") = 5, py::call_guard()) - .def("accept_connection", &TcpTransport::accept_connection, + .def("connection_count", &TcpTransport::connection_count) + .def("max_connections", &TcpTransport::max_connections) + .def("is_peer_connected", &TcpTransport::is_peer_connected, py::arg("peer")) + .def("disconnect_peer", &TcpTransport::disconnect_peer, py::arg("peer"), py::call_guard()) .def_static("is_magic_cookie", &TcpTransport::is_magic_cookie, py::arg("data"), py::arg("offset") = 0) diff --git a/src/_opensomeip/types_bind.cpp b/src/_opensomeip/types_bind.cpp index 1681dde..2b794f2 100644 --- a/src/_opensomeip/types_bind.cpp +++ b/src/_opensomeip/types_bind.cpp @@ -50,6 +50,7 @@ void init_types(py::module_& m) { .value("CONNECTION_REFUSED", Result::CONNECTION_REFUSED) .value("TIMEOUT", Result::TIMEOUT) .value("INVALID_ENDPOINT", Result::INVALID_ENDPOINT) + .value("MULTICAST_ERROR", Result::MULTICAST_ERROR) .value("INVALID_MESSAGE", Result::INVALID_MESSAGE) .value("INVALID_MESSAGE_TYPE", Result::INVALID_MESSAGE_TYPE) .value("INVALID_SERVICE_ID", Result::INVALID_SERVICE_ID) diff --git a/src/opensomeip/events.py b/src/opensomeip/events.py index 2085253..080090b 100644 --- a/src/opensomeip/events.py +++ b/src/opensomeip/events.py @@ -163,6 +163,11 @@ def __init__( ext = get_ext() if ext is not None: self._cpp = ext.events.EventSubscriber(client_id) + # v0.2.0 no longer falls back to 127.0.0.1:30490 when the + # service endpoint was not resolved. + if transport.remote_endpoint is not None: + remote = transport.remote_endpoint + self._cpp.set_default_endpoint(remote.ip, remote.port) @property def is_running(self) -> bool: diff --git a/src/opensomeip/rpc.py b/src/opensomeip/rpc.py index 8db1ced..c4d2ca2 100644 --- a/src/opensomeip/rpc.py +++ b/src/opensomeip/rpc.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: from typing_extensions import Self -from opensomeip._bridge import from_cpp_message, get_ext, to_cpp_message +from opensomeip._bridge import from_cpp_message, get_ext, to_cpp_endpoint, to_cpp_message from opensomeip.exceptions import RpcError from opensomeip.message import Message from opensomeip.receiver import MessageReceiver @@ -58,6 +58,9 @@ def __init__(self, transport: Transport, *, client_id: int = 0x0001) -> None: ext = get_ext() if ext is not None: self._cpp = ext.rpc.RpcClient(client_id) + # v0.2.0 no longer defaults application traffic to 127.0.0.1:30490. + if transport.remote_endpoint is not None: + self._cpp.set_remote_endpoint(to_cpp_endpoint(transport.remote_endpoint)) @property def is_running(self) -> bool: @@ -221,7 +224,9 @@ def __init__(self, transport: Transport, *, service_id: int = 0x0000) -> None: self._cpp: Any = None ext = get_ext() if ext is not None: - self._cpp = ext.rpc.RpcServer(service_id) + # v0.2.0 defaults this socket to 127.0.0.1:30501. Use an ephemeral + # port so it does not occupy an address the application chose. + self._cpp = ext.rpc.RpcServer(service_id, 0x01, ext.Endpoint("127.0.0.1", 0)) @property def is_running(self) -> bool: diff --git a/src/opensomeip/sd.py b/src/opensomeip/sd.py index 31de5c9..413b8f8 100644 --- a/src/opensomeip/sd.py +++ b/src/opensomeip/sd.py @@ -204,14 +204,17 @@ def find( if self._cpp is not None: receiver = self._find_receiver - def _on_found(cpp_svc: Any) -> None: - py_svc = from_cpp_service_instance(cpp_svc) - from opensomeip.message import Message - - msg = Message(payload=repr(py_svc).encode()) - receiver.put(msg) - if callback is not None: - callback(py_svc) + def _on_found(found: Any) -> None: + # Native find_service passes a sequence of ServiceInstance. + services = found if isinstance(found, (list, tuple)) else [found] + for cpp_svc in services: + py_svc = from_cpp_service_instance(cpp_svc) + from opensomeip.message import Message + + msg = Message(payload=repr(py_svc).encode()) + receiver.put(msg) + if callback is not None: + callback(py_svc) try: self._cpp.find_service(service.service_id, _on_found) diff --git a/src/opensomeip/tp.py b/src/opensomeip/tp.py index 599a314..b6ecb82 100644 --- a/src/opensomeip/tp.py +++ b/src/opensomeip/tp.py @@ -129,12 +129,31 @@ def reassembled(self) -> MessageReceiver: """Return a :class:`MessageReceiver` yielding fully reassembled messages.""" return self._reassembly_receiver - def get_statistics(self) -> Any: - """Return TP statistics (native only).""" + def get_sender_statistics(self) -> Any: + """Return sender-path TP statistics (native only).""" + if self._cpp is not None: + return self._cpp.get_sender_statistics() + return None + + def get_receiver_statistics(self) -> Any: + """Return receiver-path TP statistics (native only).""" if self._cpp is not None: - return self._cpp.get_statistics() + return self._cpp.get_receiver_statistics() return None + def get_statistics(self) -> Any: + """Return TP statistics (native only). + + opensomeip v0.2.0 reports sender and receiver counters separately. + This helper returns both snapshots. + """ + if self._cpp is None: + return None + return { + "sender": self.get_sender_statistics(), + "receiver": self.get_receiver_statistics(), + } + def __enter__(self) -> Self: self.start() return self diff --git a/tests/unit/test_coverage_gaps.py b/tests/unit/test_coverage_gaps.py index 821ed7b..335103a 100644 --- a/tests/unit/test_coverage_gaps.py +++ b/tests/unit/test_coverage_gaps.py @@ -272,6 +272,7 @@ def test_get_statistics_returns_none(self) -> None: manager = TpManager(t) with patch.object(manager, "_cpp", None): - result = manager.get_statistics() - assert result is None + assert manager.get_statistics() is None + assert manager.get_sender_statistics() is None + assert manager.get_receiver_statistics() is None t.stop()