Skip to content

feat: add SOME/IP driver wrapping opensomeip Python binding#391

Open
vtz wants to merge 5 commits intojumpstarter-dev:mainfrom
vtz:feat/someip-driver
Open

feat: add SOME/IP driver wrapping opensomeip Python binding#391
vtz wants to merge 5 commits intojumpstarter-dev:mainfrom
vtz:feat/someip-driver

Conversation

@vtz
Copy link
Copy Markdown
Contributor

@vtz vtz commented Mar 28, 2026

Summary

  • Add jumpstarter-driver-someip package providing remote access to SOME/IP (Scalable service-Oriented MiddlewarE over IP) protocol operations via the opensomeip Python binding
  • Implements RPC calls, raw messaging, service discovery (SOME/IP-SD), and event subscriptions following the same driver pattern as DoIP, UDS, and XCP drivers
  • Includes 16 passing unit tests covering all driver methods, error paths, and config validation, plus 2 integration tests (skipped by default, requiring SOMEIP_INTEGRATION_TESTS=1)

Driver API

Method Description
rpc_call() SOME/IP RPC request/response
send_message() / receive_message() Raw SOME/IP messaging
find_service() Service discovery via SOME/IP-SD
subscribe_eventgroup() / unsubscribe_eventgroup() / receive_event() Event pub/sub
close_connection() / reconnect() Connection management

Test plan

  • All 16 unit tests pass (make pkg-test-jumpstarter-driver-someip)
  • Linting passes (make lint-fix)
  • Tests cover: RPC, raw messaging, events, subscribe/unsubscribe, close, reconnect, timeout errors, connection errors, config validation, transport mode forwarding
  • Integration tests with real SOME/IP environment (skipped by default, set SOMEIP_INTEGRATION_TESTS=1)

Made with Cursor

Add jumpstarter-driver-someip package providing remote access to SOME/IP
protocol operations (RPC, service discovery, raw messaging, events) via
the opensomeip Python binding (https://pypi.org/project/opensomeip/).

Follows the same driver pattern as DoIP, UDS, and XCP drivers with
pydantic dataclass driver, @export/@validate_call methods, DriverClient
subclass, and comprehensive tests (16 passing, 2 integration skipped).

Made-with: Cursor
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 28, 2026

Deploy Preview for jumpstarter-docs failed. Why did it fail? →

Name Link
🔨 Latest commit 3500979
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69c7f2a12f3179000860ddc1

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

Adds a new jumpstarter-driver-someip package: Pydantic models for SOME/IP, a SomeIp driver wrapping opensomeip, a typed DriverClient façade, test fixtures and suites (unit + optional integration), docs, example exporter manifest, and packaging/entry-point metadata.

Changes

Cohort / File(s) Summary
Documentation & Index
python/docs/source/reference/package-apis/drivers/someip.md, python/docs/source/reference/package-apis/drivers/index.md
Added new driver reference page and registered it in the drivers index.
Package README & Examples
python/packages/jumpstarter-driver-someip/README.md, python/packages/jumpstarter-driver-someip/examples/exporter.yaml, python/packages/jumpstarter-driver-someip/.gitignore
New README describing usage/config, example exporter manifest, and .gitignore rules.
Packaging / Metadata
python/packages/jumpstarter-driver-someip/pyproject.toml
New pyproject including dependencies, dev deps, Hatch config, and jumpstarter.drivers entry-point for SomeIp.
Core Driver & Models
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py, .../common.py, .../client.py
Implemented SomeIp driver (RPC, raw messaging, discovery, events, connection control), Pydantic transport models with hex validators, and SomeIpDriverClient façade converting bytes↔hex and model types.
Testing infra & tests
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py, .../driver_test.py
Added MockSomeIpServer, StatefulOsipClient test double, pytest fixtures, extensive unit/stateful tests and optional integration tests gated by env var.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant DriverClient as SomeIpDriverClient
    participant Driver as SomeIp
    participant Osip as OsipClient
    participant Remote as RemoteService

    Client->>DriverClient: rpc_call(service_id, method_id, payload: bytes)
    DriverClient->>Driver: rpc_call(..., payload_hex, timeout)
    Driver->>Osip: call(MessageId, payload_bytes, timeout)
    Osip->>Remote: send RPC request
    Remote-->>Osip: RPC response bytes
    Osip-->>Driver: Message object
    Driver-->>DriverClient: SomeIpMessageResponse (payload hex)
    DriverClient-->>Client: SomeIpMessageResponse
Loading
sequenceDiagram
    participant Client as Client
    participant DriverClient as SomeIpDriverClient
    participant Driver as SomeIp
    participant Osip as OsipClient
    participant SD as SomeIP_SD

    Client->>DriverClient: find_service(service_id, instance_id)
    DriverClient->>Driver: find_service(...)
    Driver->>Osip: start discovery (callback)
    Osip->>SD: SD request (network)
    SD-->>Osip: discovery replies (callbacks)
    Osip-->>Driver: discovery callbacks with entries
    Driver-->>DriverClient: list[SomeIpServiceEntry]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • jumpstarter-dev/jumpstarter#439 — introduces the same pip install pattern and package index usage referenced in the new package README.

Suggested labels

backport release-0.7

Suggested reviewers

  • mangelajo
  • kirkbrauer

Poem

🐰 I hopped with hex and bytes in paw,
Wrapped opensomeip to do the claw,
Services found, events heard, messages relayed,
Tests and docs and packaging neatly laid,
Hop, hop — the SOME/IP driver is on its way!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add SOME/IP driver wrapping opensomeip Python binding' clearly and concisely describes the main change: adding a new SOME/IP driver package.
Description check ✅ Passed The description is well-related to the changeset, detailing the new driver package, its API methods, test coverage, and implementation approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py (2)

88-97: Test doesn't verify service_id is used.

The test calls client.subscribe_eventgroup(0x1234, 1) but the assertion only checks mock_client.subscribe_events.assert_called_once_with(1). The service_id parameter (0x1234) isn't verified in the mock call. If service_id is expected to be passed to the underlying library, consider adding an assertion for it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py`
around lines 88 - 97, The test test_someip_subscribe_eventgroup currently only
asserts mock_client.subscribe_events was called with (1) but doesn't verify the
service_id; update the assertion to include the service_id passed through by
SomeIp.subscribe_eventgroup (e.g., assert
mock_client.subscribe_events.assert_called_once_with(0x1234, 1) or inspect
mock_client.subscribe_events.call_args) so the test confirms SomeIp (constructor
SomeIp(...) and method subscribe_eventgroup) forwards the service_id to
OsipClient.subscribe_events.

26-44: Mock setup accesses internal implementation details.

The mock setup directly accesses mock.transport.receiver._sync_queue and mock.event_subscriber.notifications.return_value._sync_queue, which are internal implementation details of opensomeip. This tight coupling could cause test breakage if the library's internals change.

Consider documenting this dependency or abstracting it if feasible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py`
around lines 26 - 44, The test helper _make_mock_osip_client currently reaches
into opensomeip internals by setting mock.transport.receiver._sync_queue and
mock.event_subscriber.notifications.return_value._sync_queue; change the mock to
use public-facing behavior instead (e.g., set the transport receiver's public
receive/next_message method return value or configure
event_subscriber.notifications to return a mock receiver whose public
read/next/receive method yields mock_response) or wrap this internal wiring
behind a clear helper with a comment documenting the dependency; update
references inside _make_mock_osip_client so it configures the receiver via
public methods/properties (not _sync_queue) and/or add a comment explaining the
internal assumption if abstraction isn't feasible.
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py (1)

36-48: Verify the struct format string spacing.

The format string "!HHIHH BBBB" contains a space between HHIHH and BBBB. While Python's struct module ignores whitespace in format strings, this is unusual and could be confusing. Consider removing the space for clarity: "!HHIHHBBBB".

Suggested fix
     header = struct.pack(
-        "!HHIHH BBBB",
+        "!HHIHHBBBB",
         service_id,
         method_id,

And similarly on line 65:

     service_id, method_id, length, client_id, session_id, proto_ver, iface_ver, msg_type, ret_code = struct.unpack(
-        "!HHIHH BBBB", header
+        "!HHIHHBBBB", header
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`
around lines 36 - 48, The struct format string used in struct.pack in
conftest.py currently includes an unexpected space ("!HHIHH BBBB"); update the
format to remove the space (use "!HHIHHBBBB") in the header construction inside
the function where header = struct.pack(...) is called, and do the same for the
similar struct.pack call referenced around line 65 so both format strings have
no embedded whitespace for clarity.
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/common.py (1)

21-33: Consider adding hex validation to payload field.

SomeIpPayload validates that data is hex-encoded, but SomeIpMessageResponse.payload (and SomeIpEventNotification.payload) are plain str without validation. If these can contain arbitrary data from the wire, this is fine. However, if they're expected to be hex-encoded as the comment suggests, consider reusing the validator or inheriting from SomeIpPayload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/common.py`
around lines 21 - 33, SomeIpMessageResponse and SomeIpEventNotification declare
payload as plain str but the comment says it's hex-encoded; update these models
to validate/reuse the existing SomeIpPayload validator by either (A) changing
the field type from str to SomeIpPayload (so payload uses SomeIpPayload.data and
its hex validation) or (B) add a pydantic validator on
SomeIpMessageResponse.payload and SomeIpEventNotification.payload that reuses
the hex-check logic from SomeIpPayload (e.g., call the same validation helper or
import the validator function) so incoming payload values are guaranteed to be
hex-encoded; refer to the class names SomeIpMessageResponse,
SomeIpEventNotification, and SomeIpPayload to locate where to change the field
type or add the validator.
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py (3)

137-164: time.sleep(timeout) always waits the full duration.

Line 163 blocks for the entire timeout regardless of whether services are found. For service discovery this may be intentional (to gather all responses), but if early return is desirable when at least one service is found, consider using an event or condition variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`
around lines 137 - 164, The find_service implementation blocks with
time.sleep(timeout) which always waits the full duration; update find_service to
use a threading.Event or Condition instead so on_found (the callback) can signal
discovery and unblock early when appropriate: create an event before calling
self._osip_client.find, have on_found append to found and call event.set() (or
notify a condition) and then replace time.sleep(timeout) with
event.wait(timeout) (or condition.wait_for) to return as soon as at least one
service is found or timeout elapses; ensure cleanup and thread-safety around the
found list and callback registration in find_service and on_found.

62-76: Invalid transport_mode values silently default to UDP.

Line 66 only checks for "TCP" explicitly, meaning any invalid value (e.g., "FOO", "tcpp") silently becomes UDP without warning. Consider validating the value explicitly.

♻️ Proposed fix to validate transport_mode
     def __post_init__(self):
         if hasattr(super(), "__post_init__"):
             super().__post_init__()

-        mode = TransportMode.TCP if self.transport_mode.upper() == "TCP" else TransportMode.UDP
+        transport_upper = self.transport_mode.upper()
+        if transport_upper == "TCP":
+            mode = TransportMode.TCP
+        elif transport_upper == "UDP":
+            mode = TransportMode.UDP
+        else:
+            raise ValueError(f"Invalid transport_mode: {self.transport_mode!r}. Must be 'TCP' or 'UDP'.")
         config = ClientConfig(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`
around lines 62 - 76, In __post_init__, do not silently treat unknown
self.transport_mode values as UDP; instead validate self.transport_mode
(case-insensitive) against the allowed values "TCP" and "UDP", set mode to
TransportMode.TCP or TransportMode.UDP only for those valid values, and raise a
clear ValueError (or log and raise) including the invalid value when it’s
neither "TCP" nor "UDP"; update the logic around TransportMode, mode, and
ClientConfig initialization in the same method to use the validated mode.

122-133: Accessing private _sync_queue attribute is fragile.

Line 130 accesses receiver._sync_queue, which is an internal implementation detail of opensomeip. This could break if the library's internals change. Consider checking if opensomeip provides a public API for synchronous message receiving.

opensomeip python receive message API
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`
around lines 122 - 133, The method receive_message currently accesses the
private attribute receiver._sync_queue (fragile) — update receive_message to
prefer a public opensomeip API for synchronous receive (e.g., a documented
receiver.receive or receiver.get_message method) if available, and only fall
back to accessing _sync_queue via safe existence checks (use getattr(receiver,
"receive", None) or getattr(receiver, "_sync_queue", None)) while raising a
clear error if no public API or queue is present; reference the receive_message
method, the receiver object, the private _sync_queue attribute, and the
_message_to_response conversion so you ensure messages are obtained via the
public call when possible and converted with _message_to_response otherwise,
with an explicit, helpful exception if neither option exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`:
- Around line 203-208: The reconnect method can leave the client stopped if
self._osip_client.stop() raises; update reconnect (function reconnect) to catch
exceptions from self._osip_client.stop(), log or record the error similarly to
the close() implementation, and then always attempt self._osip_client.start();
if start() then fails, propagate or handle that error per close()’s pattern
(cleanup/raise) so the driver does not remain in an inconsistent stopped state
after a failed reconnect.
- Around line 166-176: The methods subscribe_eventgroup and
unsubscribe_eventgroup currently declare a service_id parameter that is never
used and not passed to
_osip_client.subscribe_events/_osip_client.unsubscribe_events; remove the unused
service_id parameter from both method signatures, update their
docstrings/signature annotations to only accept eventgroup_id: int -> None, and
adjust any decorator usage (export/validate_call) or type hints if they
reference the removed parameter; ensure references to service_id are removed
inside the methods and update any callers/tests to stop passing service_id so
the API matches the opensomeip calls.

In `@python/packages/jumpstarter-driver-someip/README.md`:
- Around line 91-94: The documentation is inconsistent: the API table lists
find_service(service_id, instance_id, timeout) but the example calls
find_service(0x1234, timeout=3.0); update the README to make the signature
consistent with the implementation by either (A) documenting that instance_id is
optional (e.g., instance_id=None) and leaving the example as-is, or (B) changing
the example to pass an explicit instance_id (e.g., find_service(0x1234, 0x0001,
timeout=3.0)); ensure the function name find_service and its parameters in the
API table and the example match exactly.

---

Nitpick comments:
In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/common.py`:
- Around line 21-33: SomeIpMessageResponse and SomeIpEventNotification declare
payload as plain str but the comment says it's hex-encoded; update these models
to validate/reuse the existing SomeIpPayload validator by either (A) changing
the field type from str to SomeIpPayload (so payload uses SomeIpPayload.data and
its hex validation) or (B) add a pydantic validator on
SomeIpMessageResponse.payload and SomeIpEventNotification.payload that reuses
the hex-check logic from SomeIpPayload (e.g., call the same validation helper or
import the validator function) so incoming payload values are guaranteed to be
hex-encoded; refer to the class names SomeIpMessageResponse,
SomeIpEventNotification, and SomeIpPayload to locate where to change the field
type or add the validator.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`:
- Around line 36-48: The struct format string used in struct.pack in conftest.py
currently includes an unexpected space ("!HHIHH BBBB"); update the format to
remove the space (use "!HHIHHBBBB") in the header construction inside the
function where header = struct.pack(...) is called, and do the same for the
similar struct.pack call referenced around line 65 so both format strings have
no embedded whitespace for clarity.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py`:
- Around line 88-97: The test test_someip_subscribe_eventgroup currently only
asserts mock_client.subscribe_events was called with (1) but doesn't verify the
service_id; update the assertion to include the service_id passed through by
SomeIp.subscribe_eventgroup (e.g., assert
mock_client.subscribe_events.assert_called_once_with(0x1234, 1) or inspect
mock_client.subscribe_events.call_args) so the test confirms SomeIp (constructor
SomeIp(...) and method subscribe_eventgroup) forwards the service_id to
OsipClient.subscribe_events.
- Around line 26-44: The test helper _make_mock_osip_client currently reaches
into opensomeip internals by setting mock.transport.receiver._sync_queue and
mock.event_subscriber.notifications.return_value._sync_queue; change the mock to
use public-facing behavior instead (e.g., set the transport receiver's public
receive/next_message method return value or configure
event_subscriber.notifications to return a mock receiver whose public
read/next/receive method yields mock_response) or wrap this internal wiring
behind a clear helper with a comment documenting the dependency; update
references inside _make_mock_osip_client so it configures the receiver via
public methods/properties (not _sync_queue) and/or add a comment explaining the
internal assumption if abstraction isn't feasible.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`:
- Around line 137-164: The find_service implementation blocks with
time.sleep(timeout) which always waits the full duration; update find_service to
use a threading.Event or Condition instead so on_found (the callback) can signal
discovery and unblock early when appropriate: create an event before calling
self._osip_client.find, have on_found append to found and call event.set() (or
notify a condition) and then replace time.sleep(timeout) with
event.wait(timeout) (or condition.wait_for) to return as soon as at least one
service is found or timeout elapses; ensure cleanup and thread-safety around the
found list and callback registration in find_service and on_found.
- Around line 62-76: In __post_init__, do not silently treat unknown
self.transport_mode values as UDP; instead validate self.transport_mode
(case-insensitive) against the allowed values "TCP" and "UDP", set mode to
TransportMode.TCP or TransportMode.UDP only for those valid values, and raise a
clear ValueError (or log and raise) including the invalid value when it’s
neither "TCP" nor "UDP"; update the logic around TransportMode, mode, and
ClientConfig initialization in the same method to use the validated mode.
- Around line 122-133: The method receive_message currently accesses the private
attribute receiver._sync_queue (fragile) — update receive_message to prefer a
public opensomeip API for synchronous receive (e.g., a documented
receiver.receive or receiver.get_message method) if available, and only fall
back to accessing _sync_queue via safe existence checks (use getattr(receiver,
"receive", None) or getattr(receiver, "_sync_queue", None)) while raising a
clear error if no public API or queue is present; reference the receive_message
method, the receiver object, the private _sync_queue attribute, and the
_message_to_response conversion so you ensure messages are obtained via the
public call when possible and converted with _message_to_response otherwise,
with an explicit, helpful exception if neither option exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09554097-1816-45a1-9399-849699ea85a2

📥 Commits

Reviewing files that changed from the base of the PR and between 03fc412 and 9ee700c.

⛔ Files ignored due to path filters (1)
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • python/docs/source/reference/package-apis/drivers/someip.md
  • python/packages/jumpstarter-driver-someip/.gitignore
  • python/packages/jumpstarter-driver-someip/README.md
  • python/packages/jumpstarter-driver-someip/examples/exporter.yaml
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/__init__.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/client.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/common.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py
  • python/packages/jumpstarter-driver-someip/pyproject.toml

- Validate transport_mode explicitly, reject invalid values with ValueError
- Remove unused service_id parameter from subscribe/unsubscribe_eventgroup
- Wrap private _sync_queue access in _receive_from_queue helper with getattr guard
- Use threading.Event in find_service for early return instead of time.sleep
- Catch stop() exceptions in reconnect() to avoid inconsistent state
- Add hex validation to payload fields in response/notification models
- Remove whitespace from struct format strings in conftest.py
- Document internal opensomeip dependency in test mock setup
- Fix README API table vs example code inconsistency
- Add test for reconnect surviving stop() failure

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py (2)

157-186: Consider race condition and multi-result handling in service discovery.

The current implementation sets the event after the first callback, which causes event.wait() to return early. If find() can discover multiple services, only the first one may be returned before the timeout expires.

If returning early after the first discovery is intentional (common for "find any matching service" use cases), consider documenting this behavior. If all matches within the timeout should be returned, the event should not be set in the callback.

💡 Option A: Document early-return behavior
     def find_service(
         self,
         service_id: int,
         instance_id: int = 0xFFFF,
         timeout: float = 5.0,
     ) -> list[SomeIpServiceEntry]:
-        """Find services via SOME/IP-SD."""
+        """Find services via SOME/IP-SD.
+        
+        Returns immediately after the first service is discovered, or after
+        the timeout if no services are found.
+        """
💡 Option B: Wait for full timeout to collect all results
-        def on_found(svc: ServiceInstance) -> None:
-            found.append(
-                SomeIpServiceEntry(
-                    service_id=svc.service_id,
-                    instance_id=svc.instance_id,
-                    major_version=svc.major_version,
-                    minor_version=svc.minor_version,
-                )
-            )
-            event.set()
+        def on_found(svc: ServiceInstance) -> None:
+            found.append(
+                SomeIpServiceEntry(
+                    service_id=svc.service_id,
+                    instance_id=svc.instance_id,
+                    major_version=svc.major_version,
+                    minor_version=svc.minor_version,
+                )
+            )

         self._osip_client.find(service, callback=on_found)
-        event.wait(timeout=timeout)
+        import time
+        time.sleep(timeout)
         return found
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`
around lines 157 - 186, The find_service implementation currently sets event in
the on_found callback which wakes event.wait() after the first discovery; decide
whether to keep early-return or collect all matches: if you want all matches
within the timeout, remove the event.set() call in on_found and rely on
event.wait(timeout) to time out (or replace event with a simple time-based loop)
so every callback appends to found without waking early; if early-return is
intended, update the find_service docstring to explicitly state "returns the
first discovered service immediately" to avoid ambiguity; relevant symbols:
find_service, on_found, self._osip_client.find, event.

44-57: Pragmatic and necessary workaround with good safeguards in place.

The code correctly documents reliance on opensomeip's internal _sync_queue attribute (verified: opensomeip Python library lacks a public blocking receive API), and includes a defensive check that fails fast if the internal API changes. This approach is sound.

Consider adding a TODO comment noting the reliance on the private API and suggesting removal once opensomeip provides a public blocking-with-timeout method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`
around lines 44 - 57, Add a TODO comment in the _receive_from_queue function
noting that it relies on the private opensomeip MessageReceiver attribute
_sync_queue and should be removed or replaced once opensomeip provides a public
blocking-with-timeout receive API; keep the existing defensive check for
getattr(receiver, "_sync_queue", None) and the TimeoutError behavior, but add a
concise TODO/TFIXME above the sync_queue retrieval referencing _sync_queue and
the opensomeip library.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py`:
- Around line 157-186: The find_service implementation currently sets event in
the on_found callback which wakes event.wait() after the first discovery; decide
whether to keep early-return or collect all matches: if you want all matches
within the timeout, remove the event.set() call in on_found and rely on
event.wait(timeout) to time out (or replace event with a simple time-based loop)
so every callback appends to found without waking early; if early-return is
intended, update the find_service docstring to explicitly state "returns the
first discovered service immediately" to avoid ambiguity; relevant symbols:
find_service, on_found, self._osip_client.find, event.
- Around line 44-57: Add a TODO comment in the _receive_from_queue function
noting that it relies on the private opensomeip MessageReceiver attribute
_sync_queue and should be removed or replaced once opensomeip provides a public
blocking-with-timeout receive API; keep the existing defensive check for
getattr(receiver, "_sync_queue", None) and the TimeoutError behavior, but add a
concise TODO/TFIXME above the sync_queue retrieval referencing _sync_queue and
the opensomeip library.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb4abd54-3fb5-40d8-acef-7f5c010c8f21

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee700c and c39395b.

📒 Files selected for processing (6)
  • python/packages/jumpstarter-driver-someip/README.md
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/client.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/common.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py
✅ Files skipped from review due to trivial changes (1)
  • python/packages/jumpstarter-driver-someip/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/common.py

vtz added 2 commits March 28, 2026 10:23
- Add StatefulOsipClient in conftest.py: a drop-in replacement for
  opensomeip.SomeIpClient that enforces SOME/IP state rules (connection
  lifecycle, service registry, event subscriptions, message ordering)
- Add 20 new stateful integration tests covering multi-step workflows:
  - RPC session: discover -> call -> verify -> disconnect
  - Messaging: send -> receive -> reconnect -> send again
  - Events: subscribe -> receive -> unsubscribe lifecycle
  - Service discovery: wildcard, specific instance, version, not-found
  - Connection management: close + reconnect, reconnect resets subs
  - Composite: discover -> RPC -> events -> cleanup
- Add receive_event timeout error test (was missing vs receive_message)
- Update README to match DoIP/XCP documentation quality with:
  - Multiple configuration examples (UDP + TCP)
  - API reference section with bullet-point descriptions
  - Separate example sections per feature area
  - Connection management example

Made-with: Cursor
Unit tests:
- test_someip_find_service: verifies find() call and callback wiring
- test_someip_find_service_no_results: empty result when no match
- test_someip_find_service_forwards_instance_id: verifies instance_id
  is passed through to the ServiceInstance

Stateful tests:
- test_stateful_find_different_services: independent results per service
- test_stateful_find_service_dynamic_registration: services added at
  runtime appear in subsequent discoveries
- test_stateful_find_service_dynamic_unregistration: removed services
  no longer appear
- test_stateful_find_service_after_reconnect: registry persists
- test_stateful_find_service_default_instance_wildcard: default 0xFFFF
  matches all instances
- test_stateful_discover_then_rpc_to_each_instance: discover -> call

Also adds register_service/unregister_service helpers to
StatefulOsipClient for dynamic registry manipulation in tests.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py`:
- Around line 210-212: The test currently checks absolute call counts which can
be satisfied by prior setup; modify the assertions in the test around
client.reconnect() to record pre-call counts (e.g., prev_start =
mock_client.start.call_count and prev_stop = mock_client.stop.call_count), call
client.reconnect(), then assert that mock_client.stop.call_count >= prev_stop +
1 and mock_client.start.call_count >= prev_start + 1 to ensure reconnect()
actually invoked stop() and start() again; locate these changes in the test
where client.reconnect() is called and adjust the assertions accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cda8b56-b357-4550-856c-a6d2aa556cf6

📥 Commits

Reviewing files that changed from the base of the PR and between c39395b and a0c2662.

📒 Files selected for processing (3)
  • python/packages/jumpstarter-driver-someip/README.md
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py
✅ Files skipped from review due to trivial changes (1)
  • python/packages/jumpstarter-driver-someip/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py

Comment on lines +210 to +212
client.reconnect()
assert mock_client.stop.call_count >= 1
assert mock_client.start.call_count >= 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Strengthen reconnect assertions to avoid false positives.

This currently passes even if reconnect() does not call start() again (because initial startup already increments start.call_count). Assert delta from pre-call counts instead.

Suggested test hardening
 `@patch`("jumpstarter_driver_someip.driver.OsipClient")
 def test_someip_reconnect(mock_osip_cls):
     mock_client = _make_mock_osip_client()
     mock_osip_cls.return_value = mock_client
 
     driver = SomeIp(host="127.0.0.1", port=30490)
     with serve(driver) as client:
+        stop_before = mock_client.stop.call_count
+        start_before = mock_client.start.call_count
         client.reconnect()
-        assert mock_client.stop.call_count >= 1
-        assert mock_client.start.call_count >= 1
+        assert mock_client.stop.call_count == stop_before + 1
+        assert mock_client.start.call_count == start_before + 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py`
around lines 210 - 212, The test currently checks absolute call counts which can
be satisfied by prior setup; modify the assertions in the test around
client.reconnect() to record pre-call counts (e.g., prev_start =
mock_client.start.call_count and prev_stop = mock_client.stop.call_count), call
client.reconnect(), then assert that mock_client.stop.call_count >= prev_stop +
1 and mock_client.start.call_count >= prev_start + 1 to ensure reconnect()
actually invoked stop() and start() again; locate these changes in the test
where client.reconnect() is called and adjust the assertions accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`:
- Around line 253-260: The fake discovery currently created by
_FakeServiceInstance does not enforce version constraints: update the fake
registry/find() implementation(s) to also compare major_version and
minor_version on _FakeServiceInstance when the lookup supplies version filters
(and treat unspecified version arguments as wildcards), so that callbacks only
fire for instances matching service_id, instance_id, and the requested
major/minor versions; apply the same change to the other find-like
implementations referenced around the other occurrences so all discovery-variant
tests respect versioned matching.
- Line 290: The fake record currently stores subscriptions in
self._subscribed_eventgroups: set[int] and never checks it when sending events;
change this to track subscriptions per service/instance (e.g.,
self._subscribed_eventgroups: dict[tuple[int,int], set[int]] keyed by
(service_id, instance_id)) and update the subscribe/unsubscribe handlers to
add/remove eventgroup IDs under the correct (service_id, instance_id) key; then,
in the event delivery paths referenced around 350-356 and 360-363 (the methods
that inject/deliver notifications), consult this map and only deliver an
injected event if its eventgroup ID is present for that service/instance. Ensure
subscribe/unsubscribe create empty sets as needed and clean up keys when empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 210547f9-4069-4b90-a976-41e3ff38480b

📥 Commits

Reviewing files that changed from the base of the PR and between c39395b and 1f79b31.

📒 Files selected for processing (3)
  • python/packages/jumpstarter-driver-someip/README.md
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py
✅ Files skipped from review due to trivial changes (2)
  • python/packages/jumpstarter-driver-someip/README.md
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py

Comment on lines +253 to +260
class _FakeServiceInstance:
"""Mimics ``opensomeip.sd.ServiceInstance``."""

def __init__(self, service_id: int, instance_id: int, major_version: int = 1, minor_version: int = 0):
self.service_id = service_id
self.instance_id = instance_id
self.major_version = major_version
self.minor_version = minor_version
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor version constraints during fake service discovery.

find() only filters on service_id and instance_id, even though the registry carries major_version / minor_version. A lookup for a specific version will still callback every matching instance, so the discovery-variant tests can pass without the driver handling versioned matches correctly.

Also applies to: 284-288, 341-348

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`
around lines 253 - 260, The fake discovery currently created by
_FakeServiceInstance does not enforce version constraints: update the fake
registry/find() implementation(s) to also compare major_version and
minor_version on _FakeServiceInstance when the lookup supplies version filters
(and treat unspecified version arguments as wildcards), so that callbacks only
fire for instances matching service_id, instance_id, and the requested
major/minor versions; apply the same change to the other find-like
implementations referenced around the other occurrences so all discovery-variant
tests respect versioned matching.

_FakeServiceInstance(0x5678, 0x0001),
]

self._subscribed_eventgroups: set[int] = set()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make injected events respect active subscriptions.

The fake records subscribed eventgroups but never uses that state when delivering notifications. As written, a test can still receive an event before subscribe or after unsubscribe, and the bare set[int] cannot distinguish the same eventgroup ID on different services/instances.

Also applies to: 350-356, 360-363

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`
at line 290, The fake record currently stores subscriptions in
self._subscribed_eventgroups: set[int] and never checks it when sending events;
change this to track subscriptions per service/instance (e.g.,
self._subscribed_eventgroups: dict[tuple[int,int], set[int]] keyed by
(service_id, instance_id)) and update the subscribe/unsubscribe handlers to
add/remove eventgroup IDs under the correct (service_id, instance_id) key; then,
in the event delivery paths referenced around 350-356 and 360-363 (the methods
that inject/deliver notifications), consult this map and only deliver an
injected event if its eventgroup ID is present for that service/instance. Ensure
subscribe/unsubscribe create empty sets as needed and clean up keys when empty.

Add SOME/IP entry to the Automotive Diagnostics Drivers section in
drivers/index.md and include someip.md in the hidden toctree so
the page appears in the sidebar on jumpstarter.dev.

The someip.md symlink to the driver README was already created by
the scaffolding script but was not wired into the docs index.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py (1)

319-327: Consider documenting that timeout is intentionally ignored.

The call() method accepts a timeout parameter to match the opensomeip API signature but doesn't use it since responses are returned immediately. A brief comment would clarify this is intentional for the test double.

📝 Suggested documentation
     def call(self, message_id, *, payload: bytes = b"", timeout: float = 5.0):
-        """Simulate an RPC call. Returns a canned response or echoes the payload."""
+        """Simulate an RPC call. Returns a canned response or echoes the payload.
+
+        Note: timeout is accepted for API compatibility but ignored (responses are immediate).
+        """
         self._require_started()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`
around lines 319 - 327, The call() test double currently accepts a timeout
parameter but ignores it; update the docstring or add a one-line inline comment
in the call method (around the def call signature and before using
_rpc_responses) stating that timeout is intentionally ignored because this fake
returns immediate responses (via _rpc_responses and _FakeMessage) to match the
opensomeip API signature for tests; no behaviour changes required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py`:
- Around line 319-327: The call() test double currently accepts a timeout
parameter but ignores it; update the docstring or add a one-line inline comment
in the call method (around the def call signature and before using
_rpc_responses) stating that timeout is intentionally ignored because this fake
returns immediate responses (via _rpc_responses and _FakeMessage) to match the
opensomeip API signature for tests; no behaviour changes required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b554e280-6e16-4639-b6d0-5d00b41e8a7a

📥 Commits

Reviewing files that changed from the base of the PR and between 1f79b31 and 3500979.

📒 Files selected for processing (3)
  • python/docs/source/reference/package-apis/drivers/index.md
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/conftest.py
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py
✅ Files skipped from review due to trivial changes (1)
  • python/packages/jumpstarter-driver-someip/jumpstarter_driver_someip/driver_test.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant