From e1567257e3231de215230ebd7c18c7104f0bac01 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 10:20:29 -0500 Subject: [PATCH 1/8] Add doctest target --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 0c556e853..c757b78c0 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,9 @@ serve-docs: clean-docs: uv run --isolated --all-packages --group docs $(MAKE) -C docs clean +doctest: + uv run --isolated --all-packages --group docs $(MAKE) -C docs doctest + test-%: packages/% uv run --isolated --directory $< pytest From 775fdfba8fcadd7414ee402939acde6f5f3dd722 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 10:35:10 -0500 Subject: [PATCH 2/8] Fix pyserial doctest --- docs/source/api-reference/drivers/pyserial.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/source/api-reference/drivers/pyserial.md b/docs/source/api-reference/drivers/pyserial.md index 0ad32c10d..3e52a9a6b 100644 --- a/docs/source/api-reference/drivers/pyserial.md +++ b/docs/source/api-reference/drivers/pyserial.md @@ -47,14 +47,22 @@ session.close() Using a simple BlockingStream with a context manager ```{testcode} with pyserialclient.stream() as stream: - stream.write(b"Hello, world!") - data = stream.read(13) + stream.send(b"Hello, world!") + data = stream.receive() ``` Using a simple BlockingStream without a context manager ```{testcode} stream = pyserialclient.open_stream() -stream.write(b"Hello, world!") -data = stream.read(13) -stream.close() +stream.send(b"Hello, world!") +data = stream.receive() +``` + +```{testsetup} * +from jumpstarter_driver_pyserial.driver import PySerial +from jumpstarter.common.utils import serve + +instance = serve(PySerial(url="loop://")) + +pyserialclient = instance.__enter__() ``` From e0366e179b948a25fe209f5a99787e13ec7ca615 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 12:06:46 -0500 Subject: [PATCH 3/8] Skip yepkit doctest --- docs/source/api-reference/drivers/yepkit.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/api-reference/drivers/yepkit.md b/docs/source/api-reference/drivers/yepkit.md index 2d42f34be..75564fd38 100644 --- a/docs/source/api-reference/drivers/yepkit.md +++ b/docs/source/api-reference/drivers/yepkit.md @@ -43,6 +43,7 @@ The yepkit ykush driver provides a `PowerClient` with the following API: ### Examples Powering on and off a device ```{testcode} +:skipif: True client.power.on() time.sleep(1) client.power.off() From f24a7818c45ba34d7d063614fc30dfdf96e280a1 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 12:45:34 -0500 Subject: [PATCH 4/8] Fix doctest of network adapters --- docs/source/api-reference/adapters/network.md | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/docs/source/api-reference/adapters/network.md b/docs/source/api-reference/adapters/network.md index 8788b8639..0a45308af 100644 --- a/docs/source/api-reference/adapters/network.md +++ b/docs/source/api-reference/adapters/network.md @@ -41,48 +41,50 @@ export: path: /tmp/test.sock ``` -Forward a remote TCP port to a local TCP port - -```{testcode} -# random port on localhost -with TcpPortforwardAdapter(client.tcp_port) as addr: - print(addr[0], addr[1]) # 127.0.0.1 38406 - -# specific address and port -with TcpPortforwardAdapter(client.tcp_port, local_host="192.0.2.1", local_port=8080) as addr: - print(addr[0], addr[1]) # 192.0.2.1 8080 +### Forward a remote TCP port to a local TCP port + +```{doctest} +>>> # random port on localhost +>>> with TcpPortforwardAdapter(client=client.tcp_port) as addr: +... print(addr[0], addr[1]) +127.0.0.1 ... +>>> +>>> # specific address and port +>>> with TcpPortforwardAdapter(client=client.tcp_port, local_host="127.0.0.2", local_port=8080) as addr: +... print(addr[0], addr[1]) +127.0.0.2 8080 ``` -Forward a remote Unix domain socket to a local socket - -```{testcode} -with UnixPortforwardAdapter(client.unix_socket) as addr: - print(addr) # /tmp/jumpstarter-w30wxu64/socket - -# the type of the remote socket and the local one doesn't have to match -# e.g. forward a remote Unix domain socket to a local TCP port -with TcpPortforwardAdapter(client.unix_socket) as addr: - print(addr[0], addr[1]) # 127.0.0.1 38406 +### Forward a remote Unix domain socket to a local socket + +```{doctest} +>>> with UnixPortforwardAdapter(client=client.unix_socket) as addr: +... print(addr) +/tmp/jumpstarter-.../socket +>>> # the type of the remote socket and the local one doesn't have to match +>>> # e.g. forward a remote Unix domain socket to a local TCP port +>>> with TcpPortforwardAdapter(client=client.unix_socket) as addr: +... print(addr[0], addr[1]) +127.0.0.1 ... ``` Connect to a remote TCP port with a web-based VNC client -```{testcode} -with NovncAdapter(client.tcp_port) as url: - print(url) # https://novnc.com/noVNC/vnc.html?autoconnect=1&reconnect=1&host=127.0.0.1&port=36459 - # open the url in browser to access the VNC client +```{doctest} +>>> with NovncAdapter(client=client.tcp_port) as url: +... print(url) # open the url in browser to access the VNC client +https://novnc.com/noVNC/vnc.html?autoconnect=1&reconnect=1&host=127.0.0.1&port=... ``` Interact with a remote TCP port as if it's a serial console See [pexpect](https://pexpect.readthedocs.io/en/stable/api/fdpexpect.html) for API documentation -```{testcode} -with PexpectAdapter(client.tcp_port) as expect: - expect.expect("localhost login:") - expect.send("root\n") - expect.expect("Password:") - expect.send("secret\n") +```{doctest} +>>> # the server echos all inputs +>>> with PexpectAdapter(client=client.tcp_port) as expect: +... assert expect.send("hello") == 5 # written 5 bytes +... assert expect.expect(["hi", "hello"]) == 1 # found string at index 1 ``` Connect to a remote TCP port with the fabric SSH client @@ -90,6 +92,18 @@ Connect to a remote TCP port with the fabric SSH client See [fabric](https://docs.fabfile.org/en/latest/api/connection.html#fabric.connection.Connection) for API documentation ```{testcode} +:skipif: True with FabricAdapter(client=client.tcp_port, connect_kwargs={"password": "secret"}) as conn: conn.run("uname") ``` + +```{testsetup} * +from jumpstarter_driver_network.adapters import * +from jumpstarter_driver_network.driver import * +from jumpstarter_driver_composite.driver import Composite +from jumpstarter.common.utils import serve + +instance = serve(Composite(children={"tcp_port": EchoNetwork(), "unix_socket": EchoNetwork()})) + +client = instance.__enter__() +``` From e8ea98e8f6331577ffe03e3465fa5e4a71b082d4 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 12:53:17 -0500 Subject: [PATCH 5/8] Fixup pyserial client cleanup --- docs/source/api-reference/drivers/pyserial.md | 6 +++++- .../jumpstarter_driver_pyserial/client.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/api-reference/drivers/pyserial.md b/docs/source/api-reference/drivers/pyserial.md index 3e52a9a6b..566237326 100644 --- a/docs/source/api-reference/drivers/pyserial.md +++ b/docs/source/api-reference/drivers/pyserial.md @@ -41,7 +41,7 @@ Using expect without a context manager session = pyserialclient.open() session.sendline("Hello, world!") session.expect("Hello, world!") -session.close() +pyserialclient.close() ``` Using a simple BlockingStream with a context manager @@ -66,3 +66,7 @@ instance = serve(PySerial(url="loop://")) pyserialclient = instance.__enter__() ``` + +```{testcleanup} * +instance.__exit__(None, None, None) +``` diff --git a/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py b/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py index cf9419d4a..fca7550ed 100644 --- a/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py +++ b/packages/jumpstarter-driver-pyserial/jumpstarter_driver_pyserial/client.py @@ -25,6 +25,10 @@ def open(self) -> fdspawn: self._context_manager = self.pexpect() return self._context_manager.__enter__() + def close(self): + if hasattr(self, "_context_manager"): + self._context_manager.__exit__(None, None, None) + @contextmanager def pexpect(self): """ From 75370198441608c37945a988dc80cd7319292c9e Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 13:01:42 -0500 Subject: [PATCH 6/8] Fix example driver doctest --- docs/source/api-reference/drivers.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/docs/source/api-reference/drivers.md b/docs/source/api-reference/drivers.md index 2f4498f63..91eaace06 100644 --- a/docs/source/api-reference/drivers.md +++ b/docs/source/api-reference/drivers.md @@ -19,16 +19,9 @@ This project is still evolving, so these docs may be incomplete or out-of-date. ``` ## Example -```{testsetup} * -import jumpstarter.common.importlib - -def import_class(class_path, allow, unsafe): - return globals()["ExampleClient"] - -jumpstarter.common.importlib.import_class = import_class -``` - ```{testcode} +from sys import modules +from types import SimpleNamespace from anyio import connect_tcp, sleep from contextlib import asynccontextmanager from collections.abc import Generator @@ -74,6 +67,8 @@ class ExampleClient(DriverClient): def echo_generator(self, message) -> Generator[str, None, None]: yield from self.streamingcall("echo_generator", message) +modules["example"] = SimpleNamespace(ExampleClient=ExampleClient) + with serve(ExampleDriver()) as client: print(client.echo("hello")) assert list(client.echo_generator("hello")) == ["hello"] * 10 From c35999dc52e53a8f69127b93491a97f2b00e7bc5 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 13:03:28 -0500 Subject: [PATCH 7/8] Make doctest part of the test target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c757b78c0..757df8a6e 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ clean-test: sync: uv sync --all-packages --all-extras -test: test-packages +test: test-packages doctest generate: buf generate From b765d8e149fbb0d094b3f31cdbf82cd6726c933a Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Fri, 7 Feb 2025 13:09:21 -0500 Subject: [PATCH 8/8] Cleanup after network adapters doctest --- docs/source/api-reference/adapters/network.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/api-reference/adapters/network.md b/docs/source/api-reference/adapters/network.md index 0a45308af..0bd8b149e 100644 --- a/docs/source/api-reference/adapters/network.md +++ b/docs/source/api-reference/adapters/network.md @@ -107,3 +107,7 @@ instance = serve(Composite(children={"tcp_port": EchoNetwork(), "unix_socket": E client = instance.__enter__() ``` + +```{testcleanup} * +instance.__exit__(None, None, None) +```