Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ doctest:
test-%: packages/%
uv run --isolated --directory $< pytest

mypy-%: packages/%
uv run --isolated --directory $< mypy .

test-packages: $(addprefix test-,$(PKG_TARGETS))

mypy-packages: $(addprefix mypy-,$(PKG_TARGETS))

clean-venv:
-rm -rf ./.venv
-find . -type d -name __pycache__ -exec rm -r {} \+
Expand All @@ -38,6 +43,8 @@ sync:

test: test-packages doctest

mypy: mypy-packages

generate:
buf generate

Expand All @@ -46,4 +53,16 @@ build:

clean: clean-docs clean-venv clean-build clean-test

.PHONY: sync docs test test-packages build clean-test clean-docs clean-venv clean-build
.PHONY: sync docs test test-packages build clean-test clean-docs clean-venv clean-build \
mypy-jumpstarter \
mypy-jumpstarter-cli-admin \
mypy-jumpstarter-cli-client \
mypy-jumpstarter-driver-can \
mypy-jumpstarter-driver-dutlink \
mypy-jumpstarter-driver-network \
mypy-jumpstarter-driver-raspberrypi \
mypy-jumpstarter-driver-sdwire \
mypy-jumpstarter-driver-tftp \
mypy-jumpstarter-driver-yepkit \
mypy-jumpstarter-kubernetes \
mypy-jumpstarter-protocol
4 changes: 2 additions & 2 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ managed:
enabled: true
plugins:
- remote: buf.build/protocolbuffers/python
out: ./packages/jumpstarter_protocol/jumpstarter_protocol
out: ./packages/jumpstarter-protocol/jumpstarter_protocol
- remote: buf.build/grpc/python
out: ./packages/jumpstarter_protocol/jumpstarter_protocol
out: ./packages/jumpstarter-protocol/jumpstarter_protocol
inputs:
- git_repo: https://github.com/jumpstarter-dev/jumpstarter-protocol.git
subdir: proto
21 changes: 21 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from contextlib import contextmanager

import pytest

try:
from jumpstarter.common.utils import serve
from jumpstarter.config import ExporterConfigV1Alpha1DriverInstance
except ImportError:
# some packages in the workspace does not depend on jumpstarter
pass
else:

@contextmanager
def run(config):
with serve(ExporterConfigV1Alpha1DriverInstance.from_str(config).instantiate()) as client:
yield client

@pytest.fixture(autouse=True)
def jumpstarter_namespace(doctest_namespace):
doctest_namespace["serve"] = serve
doctest_namespace["run"] = run
1 change: 1 addition & 0 deletions docs/source/api-reference/drivers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Drivers packages from the [drivers](https://github.com/jumpstarter-dev/jumpstart

```{toctree}
can.md
network.md
pyserial.md
sdwire.md
shell.md
Expand Down
28 changes: 28 additions & 0 deletions docs/source/api-reference/drivers/network.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Network drivers

The network drivers are a set of drivers for interacting with network servers.

## Driver configuration

```{eval-rst}
.. autoclass:: jumpstarter_driver_network.driver.TcpNetwork()
```

```{eval-rst}
.. autoclass:: jumpstarter_driver_network.driver.UdpNetwork()
```

```{eval-rst}
.. autoclass:: jumpstarter_driver_network.driver.UnixNetwork()
```

```{eval-rst}
.. autoclass:: jumpstarter_driver_network.driver.EchoNetwork()
```

## Client API

```{eval-rst}
.. autoclass:: jumpstarter_driver_network.client.NetworkClient()
:members:
```
2 changes: 2 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@
"version": "0.5.0",
"controller_version": "0.5.0",
}

doctest_test_doctest_blocks = ""
Comment thread
NickCao marked this conversation as resolved.
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Can(Driver):
"""
The CAN bus instance used for communication.
"""
bus: can.Bus = field(init=False)
bus: can.BusABC = field(init=False)

"""
A dict of cyclic send tasks to run.
Expand Down Expand Up @@ -100,6 +100,7 @@ def state(self, value: can.BusState | None = None) -> can.BusState | None:
"""
if value:
self.bus.state = value
return None
else:
return self.bus.state

Expand Down Expand Up @@ -180,7 +181,7 @@ class IsoTpPython(Driver):
"""
The CAN bus instance used for communication.
"""
bus: can.Bus = field(init=False)
bus: can.BusABC = field(init=False)

"""
The CAN bus notifier instance used to receive messages.
Expand Down
73 changes: 37 additions & 36 deletions packages/jumpstarter-driver-dutlink/examples/dutlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,40 @@

# initialize client from environment
# e.g. `jmp-exporter shell dutlink`
with env() as client:
dutlink = client.dutlink
click.secho("Connected to Dutlink", fg="green")
# apply adapter to console for expect support
with PexpectAdapter(client=dutlink.console) as console:
# stream console output to stdout
console.logfile = sys.stdout.buffer
# ensure DUT is powered off
dutlink.power.off()

click.secho("Writing system image", fg="red")
dutlink.storage.write_local_file("/tmp/nixos-visionfive2.img")
click.secho("Written system image", fg="red")

dutlink.storage.dut()
click.secho("Connected storage device to DUT", fg="green")

dutlink.power.on()
click.secho("Powered DUT on", fg="green")

click.secho("Waiting for boot menu", fg="red")
console.expect("Enter choice:")
console.sendline("1")
click.secho("Selected boot entry", fg="red")

click.secho("Waiting for login prompt", fg="red")
console.expect("nixos@nixos", timeout=300)
time.sleep(3)

reading = next(dutlink.power.read())
click.secho(f"Current power reading: {reading}", fg="blue")

console.sendline("uname -a")
console.expect("riscv64 GNU/Linux")

dutlink.power.off()
if __name__ == "__main__":
with env() as client:
dutlink = client.dutlink
click.secho("Connected to Dutlink", fg="green")
# apply adapter to console for expect support
with PexpectAdapter(client=dutlink.console) as console:
# stream console output to stdout
console.logfile = sys.stdout.buffer
# ensure DUT is powered off
dutlink.power.off()

click.secho("Writing system image", fg="red")
dutlink.storage.write_local_file("/tmp/nixos-visionfive2.img")
click.secho("Written system image", fg="red")

dutlink.storage.dut()
click.secho("Connected storage device to DUT", fg="green")

dutlink.power.on()
click.secho("Powered DUT on", fg="green")

click.secho("Waiting for boot menu", fg="red")
console.expect("Enter choice:")
console.sendline("1")
click.secho("Selected boot entry", fg="red")

click.secho("Waiting for login prompt", fg="red")
console.expect("nixos@nixos", timeout=300)
time.sleep(3)

reading = next(dutlink.power.read())
click.secho(f"Current power reading: {reading}", fg="blue")

console.sendline("uname -a")
console.expect("riscv64 GNU/Linux")

dutlink.power.off()
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class HttpServer(Driver):
"""HTTP Server driver for Jumpstarter"""

root_dir: str = "/var/www"
host: str = field(default=None)
host: str | None = field(default=None)
port: int = 8080
app: web.Application = field(init=False, default_factory=web.Application)
runner: Optional[web.AppRunner] = field(init=False, default=None)
Expand Down Expand Up @@ -203,7 +203,7 @@ def get_url(self) -> str:
return f"http://{self.host}:{self.port}"

@export
def get_host(self) -> str:
def get_host(self) -> str | None:
"""
Get the host IP address of the HTTP server.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pytest
from anyio.from_thread import start_blocking_portal

from jumpstarter.common import TemporaryTcpListener


async def echo_handler(stream):
async with stream:
while True:
try:
await stream.send(await stream.receive())
except Exception:
pass


@pytest.fixture
def tcp_echo_server():
with start_blocking_portal() as portal:
with portal.wrap_async_context_manager(TemporaryTcpListener(echo_handler, local_host="127.0.0.1")) as addr:
yield addr
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ async def connect(self): ...

@dataclass(kw_only=True)
class TcpNetwork(NetworkInterface, Driver):
'''
TcpNetwork is a driver for connecting to TCP sockets

>>> addr = getfixture("tcp_echo_server") # start a tcp echo server
>>> config = f"""
... type: jumpstarter_driver_network.driver.TcpNetwork
... config:
... host: {addr[0]} # 127.0.0.1
... port: {addr[1]} # random port
... """
>>> with run(config) as tcp:
... with tcp.stream() as conn:
... conn.send(b"hello")
... assert conn.receive() == b"hello"
'''

host: str
port: int

Expand All @@ -38,6 +54,19 @@ async def connect(self):

@dataclass(kw_only=True)
class UdpNetwork(NetworkInterface, Driver):
'''
UdpNetwork is a driver for connecting to UDP sockets

>>> config = f"""
... type: jumpstarter_driver_network.driver.UdpNetwork
... config:
... host: 127.0.0.1
... port: 41336
... """
>>> with run(config) as udp:
... pass
'''

host: str
port: int

Expand All @@ -51,6 +80,18 @@ async def connect(self):

@dataclass(kw_only=True)
class UnixNetwork(NetworkInterface, Driver):
'''
UnixNetwork is a driver for connecting to Unix domain sockets

>>> config = f"""
... type: jumpstarter_driver_network.driver.UnixNetwork
... config:
... path: /tmp/example.sock
... """
>>> with run(config) as unix:
... pass
'''

path: str

@exportstream
Expand All @@ -62,6 +103,18 @@ async def connect(self):


class EchoNetwork(NetworkInterface, Driver):
'''
EchoNetwork is a mock driver implementing the NetworkInterface

>>> config = """
... type: jumpstarter_driver_network.driver.EchoNetwork
... """
>>> with run(config) as echo:
... with echo.stream() as conn:
... conn.send(b"hello")
... assert conn.receive() == b"hello"
'''

@exportstream
@asynccontextmanager
async def connect(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@
from anyio.from_thread import start_blocking_portal

from .adapters import TcpPortforwardAdapter, UnixPortforwardAdapter
from .driver import EchoNetwork, TcpNetwork, UdpNetwork, UnixNetwork
from jumpstarter.common import TemporaryTcpListener, TemporaryUnixListener
from .driver import TcpNetwork, UdpNetwork, UnixNetwork
from jumpstarter.common import TemporaryUnixListener
from jumpstarter.common.utils import serve


def test_echo_network():
with serve(EchoNetwork()) as client:
with client.stream() as stream:
stream.send(b"hello")
assert stream.receive() == b"hello"


async def echo_handler(stream):
async with stream:
while True:
Expand All @@ -28,24 +21,13 @@ async def echo_handler(stream):
pass


def test_tcp_network():
with start_blocking_portal() as portal:
with portal.wrap_async_context_manager(TemporaryTcpListener(echo_handler, local_host="127.0.0.1")) as addr:
with serve(TcpNetwork(host=addr[0], port=addr[1])) as client:
with client.stream() as stream:
stream.send(b"hello")
assert stream.receive() == b"hello"


def test_tcp_network_portforward():
with start_blocking_portal() as portal:
with portal.wrap_async_context_manager(TemporaryTcpListener(echo_handler, local_host="127.0.0.1")) as inner:
with serve(TcpNetwork(host=inner[0], port=inner[1])) as client:
with TcpPortforwardAdapter(client=client) as addr:
stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
stream.connect(addr)
stream.send(b"hello")
assert stream.recv(5) == b"hello"
def test_tcp_network_portforward(tcp_echo_server):
with serve(TcpNetwork(host=tcp_echo_server[0], port=tcp_echo_server[1])) as client:
with TcpPortforwardAdapter(client=client) as addr:
stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
stream.connect(addr)
stream.send(b"hello")
assert stream.recv(5) == b"hello"


def test_unix_network_portforward():
Expand Down
Loading