Skip to content

Add Renode emulator driver for embedded target simulation#533

Open
vtz wants to merge 4 commits intojumpstarter-dev:mainfrom
vtz:feat/renode-driver
Open

Add Renode emulator driver for embedded target simulation#533
vtz wants to merge 4 commits intojumpstarter-dev:mainfrom
vtz:feat/renode-driver

Conversation

@vtz
Copy link
Copy Markdown
Contributor

@vtz vtz commented Apr 9, 2026

Summary

  • Introduce jumpstarter-driver-renode, a composite driver that enables Renode-based virtual hardware targets in Jumpstarter
  • Users can define any Renode-supported platform (STM32, S32K, Nucleo H753ZI, etc.) via exporter YAML configuration without modifying driver code
  • Includes RenodeMonitor (async telnet client), RenodePower (process lifecycle), RenodeFlasher (firmware loading via sysbus LoadELF/LoadBinary), and RenodeClient (composite client with CLI extension)
  • Adds ADR-0001 documenting architectural decisions: control interface (telnet monitor), UART exposure (PTY terminals), configuration model (managed mode with extra_commands), and firmware loading strategy

Design Decisions

Key architectural decisions are documented in python/docs/source/contributing/adr/0001-renode-integration.md:

Decision Choice Rationale
Control Interface Telnet Monitor Lowest-common-denominator, mirrors QEMU's QMP pattern, uses anyio.connect_tcp
UART Exposure PTY Terminals Consistent with QEMU driver's PySerial integration
Configuration Model Managed Mode Driver constructs monitor commands from YAML params; extra_commands for customization
Firmware Loading Deferred to Flash Aligns with QEMU's flash() then on() semantic

Files Changed

  • python/packages/jumpstarter-driver-renode/ — Full driver package (monitor, driver, client, tests, examples)
  • python/docs/source/contributing/adr/ — ADR index and ADR-0001 for Renode integration
  • python/docs/source/reference/package-apis/drivers/renode.md — Docs symlink to driver README

Test Plan

  • Unit tests for RenodeMonitor (connect, execute, close, timeout, error handling)
  • Unit tests for RenodePower (on/off, firmware loading, UART configuration)
  • Unit tests for RenodeFlasher (flash, format detection, not-running error)
  • Unit tests for Renode config validation (missing platform, defaults, UART config)
  • Client and CLI tests (property access, monitor command forwarding)
  • E2E test with serve() (requires Renode installed, marked with skipif)
  • CI: Add Renode installation step to python-tests.yaml (follow-up)

Made with Cursor

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 9, 2026

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

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Adds a new Renode driver package to Jumpstarter: async telnet monitor client, Renode power/flasher drivers, composite Renode driver and client CLI, extensive unit tests, package metadata, examples, README, ADR documenting design decisions and risks.

Changes

Cohort / File(s) Summary
ADR & Docs
python/docs/source/contributing/adr/0001-renode-integration.md, python/docs/source/contributing/adr/index.md, python/docs/source/reference/package-apis/drivers/renode.md
New ADR describing Renode integration scope, design decisions (telnet monitor, PTY console, YAML-managed config, flash/on semantics), ADR index page, and a reference doc linking to the package README.
Package metadata & examples
python/packages/jumpstarter-driver-renode/pyproject.toml, python/packages/jumpstarter-driver-renode/README.md, python/packages/jumpstarter-driver-renode/examples/exporter.yaml, python/packages/jumpstarter-driver-renode/.gitignore
New package pyproject, README, example exporter YAML, and .gitignore entries; registers entry points for Renode drivers.
Monitor client
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/monitor.py
Async Renode telnet monitor client with connect/retry, prompt detection, execute(command)->response, disconnect, and RenodeMonitorError.
Driver implementation
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py
Composite Renode driver plus RenodePower (process lifecycle, monitor wiring, PTY UART) and RenodeFlasher (temp firmware storage, load command handling, hot-load when running); wiring of child drivers and teardown logic.
Client & CLI
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py
RenodeClient exposing platform, uart, machine_name, monitor_cmd() and a monitor CLI subcommand.
Tests
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py
Comprehensive pytest suite covering monitor connect/execute/disconnect, power on/off/close flows, flasher behavior (hot-load, override load_command), config wiring, PTY/temp lifecycles, client/CLI behavior, plus an E2E placeholder.

Sequence Diagrams

sequenceDiagram
    participant CLI
    participant RenodeDriver as Renode Driver
    participant Power as RenodePower
    participant Monitor as RenodeMonitor
    participant RenodeProc as Renode Process
    participant PTY as PTY Terminal

    CLI->>RenodeDriver: on()
    RenodeDriver->>Power: on()
    Power->>Power: pick free port
    Power->>RenodeProc: spawn renode --monitor-port=<port>
    Power->>Monitor: connect(host=localhost, port)
    Monitor->>RenodeProc: TCP connection
    Monitor->>Monitor: read until prompt
    Monitor-->>Power: connected

    Power->>Monitor: execute("machine LoadPlatform ...")
    Monitor->>RenodeProc: send command + newline
    Monitor->>Monitor: read until prompt
    Monitor-->>Power: response

    Power->>PTY: create PTY for UART
    Power->>Monitor: execute("connector ...")
    Monitor-->>Power: connector ok

    alt firmware configured
        Power->>Monitor: execute("sysbus LoadELF <path>")
        Monitor-->>Power: load ok
    end

    Power->>Monitor: execute("start")
    Monitor-->>Power: running
    Power-->>RenodeDriver: on() complete
    RenodeDriver-->>CLI: Ready
Loading
sequenceDiagram
    participant CLI
    participant RenodeDriver as Renode Driver
    participant Flasher as RenodeFlasher
    participant Monitor as RenodeMonitor
    participant RenodeProc as Renode Process

    CLI->>RenodeDriver: flash(firmware_resource)
    RenodeDriver->>Flasher: flash(resource)
    Flasher->>Flasher: write resource -> temp file
    Flasher->>Flasher: record firmware path & load_command

    alt simulation running
        Flasher->>Monitor: execute("sysbus LoadELF <path>")
        Monitor->>RenodeProc: perform load
        Monitor-->>Flasher: success
        Flasher->>Monitor: execute("machine Reset")
        Monitor-->>Flasher: reset ok
    end

    Flasher-->>RenodeDriver: flash() complete
    RenodeDriver-->>CLI: Firmware stored/loaded
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • bennyz
  • kirkbrauer

Poem

🐰 In burrows of code I hop and sing,
Telnet whispers and PTY strings,
Firmware tucked in temp-file nests,
Reset, start—now the emulated boards take quests,
Hooray, a Renode garden springs!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main objective of the PR: adding a Renode emulator driver for embedded target simulation, which is the primary focus of all changes in the changeset.
Description check ✅ Passed The description provides a comprehensive overview of the changes, including the new driver package, architectural decisions, design choices, files changed, and test plan, all directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ 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: 4

🧹 Nitpick comments (2)
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py (1)

22-31: Accept the remainder of the CLI line as the monitor command.

Most Renode monitor commands contain spaces, so this only works naturally when the whole command is shell-quoted. Using a variadic Click argument makes ... monitor sysbus LoadELF @foo.elf`` behave as expected.

Proposed fix
     def cli(self):
         base = super().cli()

         `@base.command`(name="monitor")
-        `@click.argument`("command")
-        def monitor_command(command):
+        `@click.argument`("command", nargs=-1)
+        def monitor_command(command: tuple[str, ...]):
             """Send a command to the Renode monitor."""
-            result = self.monitor_cmd(command)
+            result = self.monitor_cmd(" ".join(command))
             if result.strip():
                 click.echo(result.strip())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py`
around lines 22 - 31, The CLI monitor handler currently defines a single-word
argument and should accept the remainder of the CLI line; update the Click
argument in cli() for the nested monitor_command (the `@base.command` handler) to
use a variadic argument (click.argument("command", nargs=-1)) and then join the
tuple into a single string before calling monitor_cmd (e.g., command_str = "
".join(command) and pass command_str to self.monitor_cmd) so multi-word Renode
monitor commands work without shell-quoting.
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py (1)

298-310: Consider renaming test for clarity.

The test name test_power_close_calls_off is misleading since close() doesn't actually call off() — it directly terminates the process without sending the quit command or disconnecting the monitor. Consider renaming to test_power_close_terminates_process to accurately reflect the behavior being tested.

Suggested rename
     `@pytest.mark.anyio`
-    async def test_power_close_calls_off(self):
-        """close() terminates the process."""
+    async def test_power_close_terminates_process(self):
+        """close() terminates the process directly without monitor cleanup."""
         driver = _make_driver()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py`
around lines 298 - 310, Rename the test function to reflect actual behavior:
change the test named test_power_close_calls_off to
test_power_close_terminates_process and update its reference in the file (the
async test function that calls RenodePower.close and asserts
mock_process.terminate was called and power._process is None) so the name
accurately describes that close() terminates the process rather than calling
off().
🤖 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-renode/jumpstarter_driver_renode/driver.py`:
- Around line 99-127: The block that starts the Renode process with Popen and
then performs monitor setup (Popen -> self._process, RenodeMonitor,
self._monitor.connect, and subsequent self._monitor.execute calls) must be
wrapped in a try/except so any exception during monitor connection or commands
will teardown the spawned subprocess to avoid leaking it and leaving
self._process set. Modify the code so after creating self._process you use try:
... to run the RenodeMonitor connect and all execute calls (including load and
start), and in except Exception as e: ensure you cleanly stop the subprocess
(call self._process.terminate()/kill(), wait() and close pipes), set
self._process = None, then re-raise the exception; reference the symbols Popen,
self._process, RenodeMonitor, self._monitor.connect, self._monitor.execute and
ensure cleanup mirrors what off() would do.
- Around line 98-99: The Popen invocation in driver.py is piping
stdin/stdout/stderr (self._process = Popen(..., stdin=PIPE, stdout=PIPE,
stderr=PIPE)) which are never read and can cause the Renode process to block;
change those three arguments to use subprocess.DEVNULL instead so the child
won't deadlock (import DEVNULL if needed) — update the Popen call where
self._process is created and ensure any related code expecting piped streams is
removed or adjusted.
- Around line 144-148: The shutdown path is blocking because it calls the
synchronous Popen.wait(timeout=5) (used alongside self._process.terminate() and
except TimeoutExpired -> self._process.kill()) inside an async context (off());
change this to a non-blocking approach by running the blocking wait/killing
logic off the event loop (e.g., wrap the wait/kill sequence in a thread via
asyncio.to_thread or AnyIO's thread-runner) or replace it with an async poll
loop using self._process.poll() plus async sleep and then call kill if still
running; update the code around self._process.terminate(), self._process.wait(),
TimeoutExpired, and self._process.kill() accordingly so off() never blocks the
event loop.

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/monitor.py`:
- Around line 43-52: The execute method currently returns raw Renode responses;
update execute (in jumpstarter_driver_renode.monitor.Monitor.execute) to inspect
the text returned by _read_until_prompt and, if it indicates a command failure
(e.g., contains "ERROR", "Error", "Failed", or matches Renode's error prefix),
raise RenodeMonitorError with the full response instead of returning it; keep
logging the request/response but throw RenodeMonitorError to stop initialization
(adjust tests like test_monitor_execute_error_response and callers such as
RenodePower.on which rely on execute's behavior to handle failures).

---

Nitpick comments:
In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py`:
- Around line 22-31: The CLI monitor handler currently defines a single-word
argument and should accept the remainder of the CLI line; update the Click
argument in cli() for the nested monitor_command (the `@base.command` handler) to
use a variadic argument (click.argument("command", nargs=-1)) and then join the
tuple into a single string before calling monitor_cmd (e.g., command_str = "
".join(command) and pass command_str to self.monitor_cmd) so multi-word Renode
monitor commands work without shell-quoting.

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py`:
- Around line 298-310: Rename the test function to reflect actual behavior:
change the test named test_power_close_calls_off to
test_power_close_terminates_process and update its reference in the file (the
async test function that calls RenodePower.close and asserts
mock_process.terminate was called and power._process is None) so the name
accurately describes that close() terminates the process rather than calling
off().
🪄 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: d349f643-86a8-49ef-8781-30c12c5cb166

📥 Commits

Reviewing files that changed from the base of the PR and between 8896671 and afc136f.

📒 Files selected for processing (12)
  • python/docs/source/contributing/adr/0001-renode-integration.md
  • python/docs/source/contributing/adr/index.md
  • python/docs/source/reference/package-apis/drivers/renode.md
  • python/packages/jumpstarter-driver-renode/.gitignore
  • python/packages/jumpstarter-driver-renode/README.md
  • python/packages/jumpstarter-driver-renode/examples/exporter.yaml
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/__init__.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/monitor.py
  • python/packages/jumpstarter-driver-renode/pyproject.toml

ambient-code bot pushed a commit that referenced this pull request Apr 9, 2026
- Add structured Design Decisions (DD-N) section to JEP template following
  the ADR pattern used in the project (e.g., ADR-0001 from PR #533)
- Add Consequences section (positive/negative/risks) to JEP template
- Mark all template sections as mandatory, optional, or conditional
- Document the relationship between JEPs and ADRs in JEP-0000
- Add SpecKit and ADR references to Prior Art in JEP-0000
- Add agent instructions and document conventions to jeps/README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
vtz added 2 commits April 11, 2026 13:32
Introduce jumpstarter-driver-renode, a composite driver that enables
Renode-based virtual hardware targets in Jumpstarter. Users can define
any Renode-supported platform (STM32, S32K, Nucleo H753ZI, etc.) via
exporter YAML configuration without modifying driver code.

Key components:
- RenodeMonitor: async telnet client for the Renode monitor interface
- RenodePower: manages Renode process lifecycle and simulation control
- RenodeFlasher: firmware loading via sysbus LoadELF / LoadBinary
- RenodeClient: composite client with CLI extension for monitor commands

Includes ADR-0001 documenting architectural decisions (control interface,
UART exposure, configuration model, firmware loading strategy).

Made-with: Cursor
- Replace PIPE with DEVNULL in Popen to prevent deadlocks (pipes
  were never read)
- Wrap monitor setup in try-except to teardown subprocess on failure,
  preventing process leaks
- Use anyio.to_thread.run_sync for blocking wait() in off() to avoid
  blocking the event loop
- Raise RenodeMonitorError on error responses instead of silently
  returning error text
- Accept multi-word monitor commands in CLI via nargs=-1
- Rename test_power_close_calls_off to test_power_close_terminates_process
- Add docstrings across all public APIs

Made-with: Cursor
@vtz vtz force-pushed the feat/renode-driver branch from fd842df to 742c714 Compare April 11, 2026 17:33
Restructure the Renode integration ADR to follow the JEP template
format: metadata table, Abstract, Motivation, Rejected Alternatives,
Prior Art, Implementation History, and References sections. The DD-N
design decisions and Consequences sections were already aligned.

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

🧹 Nitpick comments (2)
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py (2)

40-72: Simplify the running-state check in flash().

Line 62 uses hasattr(self.parent.children["power"], "_process"), but _process is always defined as a dataclass field with default=None. This check always returns True, making it misleading. The effective guard is if monitor is not None on line 64.

Consider replacing with a more explicit check:

Proposed fix
-        if hasattr(self.parent.children["power"], "_process"):
-            monitor = self.parent.children["power"]._monitor
-            if monitor is not None:
+        power = self.parent.children["power"]
+        if power._process is not None and power._monitor is not None:
+            monitor = power._monitor
+            if True:  # already checked above

Or more simply:

-        if hasattr(self.parent.children["power"], "_process"):
-            monitor = self.parent.children["power"]._monitor
-            if monitor is not None:
+        monitor = self.parent.children["power"]._monitor
+        if monitor is not None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py`
around lines 40 - 72, The hasattr(self.parent.children["power"], "_process")
check in RenodeFlasher.flash is misleading because _process is always present;
remove that hasattr guard and instead explicitly ensure the power child and its
monitor exist before hot-loading: fetch the power child (e.g. power =
self.parent.children.get("power")), check power is not None and power._monitor
is not None, then assign monitor = power._monitor and run monitor.execute(...)
as currently written; update the RenodeFlasher.flash method to use these
explicit checks (referencing RenodeFlasher.flash, self.parent.children["power"],
_monitor and _process to locate the code).

166-177: Monitor socket not explicitly closed in synchronous close().

The close() method sets self._monitor = None without closing the underlying TCP socket. While the socket will likely be broken when Renode terminates, it's not explicitly cleaned up.

Since close() must be synchronous (per the learning, it's called during driver teardown outside an async context) and disconnect() is async, a potential workaround is to directly close the socket if available:

Possible enhancement
     def close(self):
         """Synchronous cleanup for use during driver teardown."""
         if self._process is not None:
             if self._monitor is not None:
+                # Best-effort sync close of the socket
+                if self._monitor._stream is not None:
+                    try:
+                        self._monitor._stream._transport.close()
+                    except Exception:
+                        pass
                 self._monitor = None

This is a minor improvement — the socket will be cleaned up when Renode terminates anyway, but explicit cleanup is cleaner.

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

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py`
around lines 166 - 177, The close() method currently nulled out self._monitor
without closing the underlying socket; modify close() to synchronously close the
monitor if present (e.g., call self._monitor.close() or the monitor's
appropriate close/shutdown method) inside a try/except to swallow/LOG any
exceptions, then set self._monitor = None, and proceed to terminate the process
as before; reference the close() method and the self._monitor attribute (and
keep disconnect() untouched since it is async).
🤖 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-renode/jumpstarter_driver_renode/driver_test.py`:
- Around line 318-398: The async iterator mocks in TestRenodeFlasher tests
(test_flash_stores_firmware_path, test_flash_while_running_sends_load_and_reset,
test_flash_custom_load_command) use AsyncMock(return_value=iter(...)) which
doesn't implement async iteration; update each patched flasher.resource mock
(the mock_res used in those with patch.object(flasher, "resource")) to implement
async iteration by assigning mock_res.__aiter__ = lambda self: self and
mock_res.__anext__ = AsyncMock(side_effect=[<chunks...>, StopAsyncIteration()])
so the async for chunk in res: loops in RenodeFlasher.flash() iterate correctly
and yield the desired byte chunks.

---

Nitpick comments:
In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py`:
- Around line 40-72: The hasattr(self.parent.children["power"], "_process")
check in RenodeFlasher.flash is misleading because _process is always present;
remove that hasattr guard and instead explicitly ensure the power child and its
monitor exist before hot-loading: fetch the power child (e.g. power =
self.parent.children.get("power")), check power is not None and power._monitor
is not None, then assign monitor = power._monitor and run monitor.execute(...)
as currently written; update the RenodeFlasher.flash method to use these
explicit checks (referencing RenodeFlasher.flash, self.parent.children["power"],
_monitor and _process to locate the code).
- Around line 166-177: The close() method currently nulled out self._monitor
without closing the underlying socket; modify close() to synchronously close the
monitor if present (e.g., call self._monitor.close() or the monitor's
appropriate close/shutdown method) inside a try/except to swallow/LOG any
exceptions, then set self._monitor = None, and proceed to terminate the process
as before; reference the close() method and the self._monitor attribute (and
keep disconnect() untouched since it is async).
🪄 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: 381d75b0-27c9-41e5-9df3-3869a5a95ad9

📥 Commits

Reviewing files that changed from the base of the PR and between afc136f and f7e8718.

⛔ Files ignored due to path filters (1)
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • python/docs/source/contributing/adr/0001-renode-integration.md
  • python/docs/source/contributing/adr/index.md
  • python/docs/source/reference/package-apis/drivers/renode.md
  • python/packages/jumpstarter-driver-renode/.gitignore
  • python/packages/jumpstarter-driver-renode/README.md
  • python/packages/jumpstarter-driver-renode/examples/exporter.yaml
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/__init__.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/monitor.py
  • python/packages/jumpstarter-driver-renode/pyproject.toml
✅ Files skipped from review due to trivial changes (7)
  • python/docs/source/reference/package-apis/drivers/renode.md
  • python/packages/jumpstarter-driver-renode/.gitignore
  • python/docs/source/contributing/adr/index.md
  • python/packages/jumpstarter-driver-renode/examples/exporter.yaml
  • python/packages/jumpstarter-driver-renode/pyproject.toml
  • python/packages/jumpstarter-driver-renode/README.md
  • python/docs/source/contributing/adr/0001-renode-integration.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/client.py

@vtz
Copy link
Copy Markdown
Contributor Author

vtz commented Apr 11, 2026

@kirkbrauer ADR-0001 has been restructured to align with the JEP template format from #423 (metadata table, Abstract, Motivation, Rejected Alternatives, Prior Art, Implementation History, References, and license footer). The DD-N design decisions and Consequences sections were already in the right shape.

Replace incorrect AsyncMock(return_value=iter([...])) with idiomatic
__aiter__/__anext__ pattern. The old pattern silently yielded nothing
(with RuntimeWarning), the new one properly iterates the chunks.

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

🧹 Nitpick comments (1)
python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py (1)

241-248: Strengthen idempotency tests with observable assertions.

Both tests currently pass on “no exception” only. Add explicit assertions that no startup/shutdown side effects were triggered (or assert expected warning logs), so regressions are caught deterministically.

🧪 Suggested direction
 `@pytest.mark.anyio`
 async def test_power_on_idempotent(self):
     """Second on() call logs warning and does nothing."""
     driver = _make_driver()
     power: RenodePower = driver.children["power"]
     power._process = MagicMock()

-    await power.on()
+    with patch("jumpstarter_driver_renode.driver._find_renode") as mock_find_renode:
+        await power.on()
+    mock_find_renode.assert_not_called()

 `@pytest.mark.anyio`
 async def test_power_off_idempotent(self):
     """Second off() call logs warning and does nothing."""
     driver = _make_driver()
     power: RenodePower = driver.children["power"]
     power._process = None

-    await power.off()
+    await power.off()  # no-op path
+    assert power._process is None

As per coding guidelines: python/packages/jumpstarter-driver-*/jumpstarter_driver_*/driver_test.py: “Each driver package must include comprehensive tests in driver_test.py.”

Also applies to: 290-297

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

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py`
around lines 241 - 248, The idempotency tests (e.g., test_power_on_idempotent
and the corresponding power.off test around lines 290-297) currently only await
calls; enhance them to assert observable behavior: verify that
RenodePower._process (accessed via power._process) was not called when calling
power.on() or power.off() a second time, and assert that the expected warning
log was emitted (mock the logger or use caplog) to ensure the function logged a
warning rather than performing startup/shutdown side effects; update both tests
to include these assertions so regressions are caught deterministically.
🤖 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-renode/jumpstarter_driver_renode/driver_test.py`:
- Around line 445-449: The test test_renode_pty_path should use
pathlib.Path-based assertions to mirror the driver implementation: replace the
string suffix check and manual substring check with a single Path
equality/assertion that Path(driver._pty) == Path(driver._tmp_dir.name) / "pty"
(or assert Path(driver._pty).resolve() == (Path(driver._tmp_dir.name) /
"pty").resolve()) so the test uses the same Path construction as the driver
(referencing driver._pty and driver._tmp_dir).

---

Nitpick comments:
In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py`:
- Around line 241-248: The idempotency tests (e.g., test_power_on_idempotent and
the corresponding power.off test around lines 290-297) currently only await
calls; enhance them to assert observable behavior: verify that
RenodePower._process (accessed via power._process) was not called when calling
power.on() or power.off() a second time, and assert that the expected warning
log was emitted (mock the logger or use caplog) to ensure the function logged a
warning rather than performing startup/shutdown side effects; update both tests
to include these assertions so regressions are caught deterministically.
🪄 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: 91eeec4c-c940-43e5-87c4-52dfc245331c

📥 Commits

Reviewing files that changed from the base of the PR and between f7e8718 and e19be08.

📒 Files selected for processing (1)
  • python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py

Comment on lines +445 to +449
def test_renode_pty_path(self):
"""PTY path is inside the temp directory."""
driver = _make_driver()
assert driver._pty.endswith("/pty")
assert driver._tmp_dir.name in driver._pty
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether CI runs Python jobs on Windows (portability relevance check).
fd -i '.*\.(yml|yaml)$' .github/workflows \
  | xargs rg -n -C2 'runs-on:\s*windows|windows-latest|matrix:|os:'

Repository: jumpstarter-dev/jumpstarter

Length of output: 3429


🏁 Script executed:

# Check the driver implementation to understand _pty and platform specifics
cat -n python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py | head -100

Repository: jumpstarter-dev/jumpstarter

Length of output: 4103


🏁 Script executed:

# Search for _pty initialization and usage
rg -n '_pty' python/packages/jumpstarter-driver-renode/

Repository: jumpstarter-dev/jumpstarter

Length of output: 855


🏁 Script executed:

# Check for platform-specific imports or guards
rg -n 'import os|import sys|import platform|sys.platform|platform.system' python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py

Repository: jumpstarter-dev/jumpstarter

Length of output: 53


🏁 Script executed:

# Check if there's any Windows-related logic or comments
rg -n -i 'windows|win32|win64|msvc|pty|tty|posix' python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver.py

Repository: jumpstarter-dev/jumpstarter

Length of output: 361


Use Path-based assertions for consistency with the implementation.

The test hardcodes /pty for string comparison, while the driver implementation (line 221-222 in driver.py) constructs the path using Path(self._tmp_dir.name) / "pty". The test assertions should mirror the implementation's approach for consistency.

Note: The Renode driver is POSIX-only since PTY (pseudo-terminal) is not a Windows feature, so this is a code style concern rather than a cross-platform portability issue.

🔧 Proposed fix
     def test_renode_pty_path(self):
         """PTY path is inside the temp directory."""
         driver = _make_driver()
-        assert driver._pty.endswith("/pty")
-        assert driver._tmp_dir.name in driver._pty
+        pty_path = Path(driver._pty)
+        assert pty_path.name == "pty"
+        assert driver._tmp_dir.name in str(pty_path.parent)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-renode/jumpstarter_driver_renode/driver_test.py`
around lines 445 - 449, The test test_renode_pty_path should use
pathlib.Path-based assertions to mirror the driver implementation: replace the
string suffix check and manual substring check with a single Path
equality/assertion that Path(driver._pty) == Path(driver._tmp_dir.name) / "pty"
(or assert Path(driver._pty).resolve() == (Path(driver._tmp_dir.name) /
"pty").resolve()) so the test uses the same Path construction as the driver
(referencing driver._pty and driver._tmp_dir).

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