Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 2eeefd7

Browse files
authored
Merge branch 'main' into lease-monitor
2 parents f260968 + 923d9b6 commit 2eeefd7

3 files changed

Lines changed: 105 additions & 11 deletions

File tree

packages/jumpstarter-driver-composite/jumpstarter_driver_composite/driver.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dataclasses import field
12
from functools import reduce
23

34
from pydantic.dataclasses import dataclass
@@ -20,22 +21,33 @@ class Composite(CompositeInterface, Driver):
2021
@dataclass(kw_only=True)
2122
class Proxy(Driver):
2223
ref: str
24+
_proxy_target: Driver | None = field(default=None, init=False, repr=False)
2325

2426
@classmethod
2527
def client(cls) -> str:
26-
return "jumpstarter.client.DriverClient" # unused
28+
raise NotImplementedError("Proxy.client() should never be called; report() delegates to target")
2729

28-
def __target(self, root, name):
30+
def _resolve_proxy_target(self, root, name):
31+
if self._proxy_target:
32+
return self._proxy_target
2933
try:
3034
path = self.ref.split(".")
3135
if not path:
3236
raise ConfigurationError(f"Proxy driver {name} has empty path")
33-
return reduce(lambda instance, name: instance.children[name], path, root)
37+
self._proxy_target = reduce(lambda instance, name: instance.children[name], path, root)
38+
return self._proxy_target
3439
except KeyError:
3540
raise ConfigurationError(f"Proxy driver {name} references nonexistent driver {self.ref}") from None
3641

37-
def report(self, *, root=None, parent=None, name=None):
38-
return self.__target(root, name).report(root=root, parent=parent, name=name)
42+
def report(self, *, parent=None, name=None):
43+
if not self._proxy_target:
44+
raise RuntimeError("Proxy target not resolved. Call enumerate() before report()")
45+
return self._proxy_target.report(parent=parent, name=name)
3946

4047
def enumerate(self, *, root=None, parent=None, name=None):
41-
return self.__target(root, name).enumerate(root=root, parent=parent, name=name)
48+
return self._resolve_proxy_target(root or self, name).enumerate(root=root or self, parent=parent, name=name)
49+
50+
def __getattr__(self, name):
51+
if not self._proxy_target:
52+
raise RuntimeError(f"Proxy target not resolved. Call enumerate() before accessing '{name}'")
53+
return getattr(self._proxy_target, name)

packages/jumpstarter-driver-composite/jumpstarter_driver_composite/driver_test.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
11
from jumpstarter_driver_power.driver import MockPower
2+
from pydantic.dataclasses import dataclass
23

34
from .driver import Composite, Proxy
45
from jumpstarter.common.utils import serve
6+
from jumpstarter.driver import Driver, export
7+
8+
9+
# Mock serial driver with a connect() method
10+
@dataclass(kw_only=True)
11+
class MockSerial(Driver):
12+
connected: bool = False
13+
14+
@classmethod
15+
def client(cls) -> str:
16+
return "jumpstarter.client.DriverClient"
17+
18+
@export
19+
def connect(self):
20+
self.connected = True
21+
return "connected"
22+
23+
@export
24+
def read(self):
25+
return "data"
26+
27+
28+
# Mock parent driver that accesses proxy child methods
29+
@dataclass(kw_only=True)
30+
class MockParent(Driver):
31+
@classmethod
32+
def client(cls) -> str:
33+
return "jumpstarter.client.DriverClient"
34+
35+
@export
36+
def initialize(self):
37+
# This simulates RideSX accessing self.children["serial"].connect()
38+
result = self.children["serial"].connect()
39+
return f"initialized with {result}"
540

641

742
def test_drivers_composite():
@@ -23,3 +58,54 @@ def test_drivers_composite():
2358
client.composite1.power1.on()
2459
client.proxy0.on()
2560
client.proxy1.power1.on()
61+
62+
63+
def test_proxy_method_forwarding():
64+
"""Test that Proxy forwards method calls to target driver"""
65+
# Server-side test: verify __getattr__ works on Proxy
66+
actual_serial = MockSerial()
67+
proxy = Proxy(ref="test")
68+
composite = Composite(
69+
children={
70+
"proxy_serial": proxy,
71+
"test": actual_serial,
72+
}
73+
)
74+
75+
# Simulate enumerate() being called (happens during serve())
76+
composite.enumerate()
77+
78+
# Now test that proxy forwards method calls to target
79+
result = proxy.connect()
80+
assert result == "connected"
81+
assert actual_serial.connected is True
82+
83+
data = proxy.read()
84+
assert data == "data"
85+
86+
87+
def test_proxy_in_parent_child():
88+
"""Test that parent driver can call methods on Proxy child (RideSX scenario)"""
89+
# Server-side test: verify parent accessing self.children["serial"].method()
90+
actual_serial = MockSerial()
91+
proxy = Proxy(ref="actual_serial")
92+
parent = MockParent(
93+
children={
94+
"serial": proxy,
95+
}
96+
)
97+
composite = Composite(
98+
children={
99+
"parent": parent,
100+
"actual_serial": actual_serial,
101+
}
102+
)
103+
104+
# Simulate enumerate() being called (happens during serve())
105+
composite.enumerate()
106+
107+
# Now test that parent.initialize() works, which internally calls
108+
# self.children["serial"].connect() on the Proxy
109+
result = parent.initialize()
110+
assert result == "initialized with connected"
111+
assert actual_serial.connected is True

packages/jumpstarter/jumpstarter/driver/base.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,12 @@ async def Stream(self, request, context):
195195
) as stream:
196196
yield stream
197197

198-
def report(self, *, root=None, parent=None, name=None):
198+
def report(self, *, parent=None, name=None):
199199
"""
200200
Create DriverInstanceReport
201201
202202
:meta private:
203203
"""
204-
205-
if root is None:
206-
root = self
207-
208204
return jumpstarter_pb2.DriverInstanceReport(
209205
uuid=str(self.uuid),
210206
parent_uuid=str(parent.uuid) if parent else None,

0 commit comments

Comments
 (0)