Skip to content

Android ADB and Emulator Power Drivers#403

Merged
mangelajo merged 3 commits intomainfrom
feat/jumpstarter-driver-adb
Apr 6, 2026
Merged

Android ADB and Emulator Power Drivers#403
mangelajo merged 3 commits intomainfrom
feat/jumpstarter-driver-adb

Conversation

@kirkbrauer
Copy link
Copy Markdown
Member

This pull request adds basic support for Android devices through the jumpstarter-driver-adb and jumpstarter-driver-androidemulator. I have also added an example under python/examples/android-emulator that shows how we can utilize these drivers together to demonstrate Android support.

The ADB driver is indented to be used in more complex Android testing tools such as Android Studio, Trade Federation (tradefed), or Appium to prepare the remote target device so those tools can behave as if the device is connected physically. In the future, we might be able to offer deeper integration with these tools.

The Android Emulator power driver provides the ability to launch an Android Virtual Device (AVD) just like the QEMU driver does for QEMU virtual machines. This enables us to write basic tests against Android devices for demonstration purposes.

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 6, 2026

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit eef5a87
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69d315d90044180008bb0111
😎 Deploy Preview https://deploy-preview-403--jumpstarter-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds two new drivers—jumpstarter-driver-adb and jumpstarter-driver-androidemulator—with implementations, clients, tests, docs, example ExporterConfig manifests, an Android-emulator example/test-suite, workspace pyproject entries, and helper scripts for AVD setup.

Changes

Cohort / File(s) Summary
ADB Driver Implementation
python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver.py, python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/client.py, python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver_test.py
Adds AdbServer driver and AdbClient CLI/client: validates adb binary/port, (auto)starts/kills adb server, lists devices, and forwards remote ADB via TCP port forwarding. Includes unit tests mocking subprocess/path interactions.
ADB Packaging & Examples
python/packages/jumpstarter-driver-adb/pyproject.toml, .../README.md, .../examples/exporter.yaml, .../.gitignore
New package metadata, entry-point registration (jumpstarter.driversAdbServer), README with CLI/API docs and usage notes, example ExporterConfig, and .gitignore additions.
Android Emulator Driver Implementation
python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver.py, .../client.py, .../driver_test.py
Adds AndroidEmulator composite driver (children: AdbServer, AndroidEmulatorPower) and AndroidEmulatorClient. Implements emulator process lifecycle (on/off/read), headless handling, adb tunneling, boot polling, and tests covering validation and lifecycle behavior.
Android Emulator Packaging & Examples
python/packages/jumpstarter-driver-androidemulator/pyproject.toml, .../README.md, .../examples/*.yaml, .../.gitignore
New package metadata, entry-point registration for AndroidEmulator and AndroidEmulatorPower, README with configuration/usage/API, and example ExporterConfig manifests.
Example Android Emulator Tests & Setup
python/examples/android-emulator/README.md, python/examples/android-emulator/setup.sh, python/examples/android-emulator/pyproject.toml, python/examples/android-emulator/jumpstarter_example_android_emulator/conftest.py, .../test_android_emulator.py
Adds example project with AVD setup script, pytest project config, session fixtures to serve emulator and provide an adb_device context, and integration tests exercising properties, packages, activity launch, file push/pull, display/battery info, and adb listing.
Docs & Workspace Integration
python/docs/source/reference/package-apis/drivers/adb.md, python/docs/source/reference/package-apis/drivers/androidemulator.md, python/docs/source/reference/package-apis/drivers/index.md, python/pyproject.toml
Adds driver reference pages pointing to package READMEs, updates drivers index to include both drivers in appropriate categories, and registers both packages as workspace sources in python/pyproject.toml.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AdbClient
    participant TcpPortforwardAdapter
    participant RemoteAdbServer

    Client->>AdbClient: invoke adb subcommand
    AdbClient->>AdbClient: check persistent tunnel state
    alt Tunnel active and usable
        AdbClient->>TcpPortforwardAdapter: reuse existing tunnel (host,port)
    else No active tunnel
        AdbClient->>TcpPortforwardAdapter: create ephemeral forward (remote ADB -> local TCP)
    end
    TcpPortforwardAdapter->>RemoteAdbServer: establish port forward
    RemoteAdbServer-->>TcpPortforwardAdapter: return local (host,port)
    AdbClient->>AdbClient: set ANDROID_ADB_SERVER_ADDRESS/PORT for subprocess
    AdbClient->>Client: run local adb subprocess with env
    AdbClient-->>Client: return adb result
Loading
sequenceDiagram
    participant TestClient
    participant AndroidEmulatorClient
    participant AdbServer
    participant AndroidEmulatorPower
    participant EmulatorProcess

    TestClient->>AndroidEmulatorClient: set_headless(true)
    AndroidEmulatorClient->>AndroidEmulatorPower: store headless flag

    TestClient->>AndroidEmulatorClient: request adb_device()
    AndroidEmulatorClient->>AdbServer: forward_adb(port=0)
    AdbServer->>EmulatorProcess: open tunnel to emulator ADB
    EmulatorProcess-->>AdbServer: return (host,port)
    AndroidEmulatorClient->>AndroidEmulatorClient: create adbutils.AdbClient(host,port)
    AndroidEmulatorClient->>AndroidEmulatorClient: poll getprop sys.boot_completed until "1"
    AndroidEmulatorClient->>AndroidEmulatorClient: list_devices() and validate presence
    AndroidEmulatorClient-->>TestClient: yield adb_device context
    TestClient->>TestClient: run device tests (push/pull/shell)
    TestClient->>AndroidEmulatorClient: exit context (cleanup)
    AndroidEmulatorClient->>AdbServer: close tunnel
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, python

Suggested reviewers

  • raballew

Poem

🐇 I tunneled through ports with glee,
ADB and emu now join me.
I boot, I push, I list, I play—
Docs, tests, and scripts light the way.
Hooray for hops and CI tea!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.39% 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 'Android ADB and Emulator Power Drivers' clearly and concisely summarizes the main changes: introduction of two new driver packages for Android device support.
Description check ✅ Passed The description is directly relevant, explaining the addition of jumpstarter-driver-adb and jumpstarter-driver-androidemulator packages, their intended use cases (integrating with Android testing tools), and the included example demonstrating their usage.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/jumpstarter-driver-adb

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 (15)
python/packages/jumpstarter-driver-adb/examples/exporter.yaml (1)

1-19: LGTM! Clear example configuration.

The exporter config is well-documented with helpful comments explaining prerequisites. The configuration correctly specifies the ADB server connection parameters. The omission of adb_path is intentional and will use the default resolution via PATH.

Minor: Add trailing newline

Consider adding a trailing newline for POSIX text file compliance.

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

In `@python/packages/jumpstarter-driver-adb/examples/exporter.yaml` around lines 1
- 19, Add a POSIX-compliant trailing newline to the YAML example for the
ExporterConfig named "adb-local" (apiVersion jumpstarter.dev/v1alpha1, kind
ExporterConfig) by ensuring the file ends with a single newline character after
the final "port: 15037" line; this is a purely formatting change to the
examples/exporter.yaml content so text tools and linters recognize it as a POSIX
text file.
python/packages/jumpstarter-driver-androidemulator/.gitignore (1)

1-3: LGTM! Standard Python ignore patterns.

The ignore patterns correctly exclude Python cache and coverage artifacts. Optionally, consider adding a trailing newline for POSIX compliance.

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

In `@python/packages/jumpstarter-driver-androidemulator/.gitignore` around lines 1
- 3, The .gitignore contents correctly list __pycache__/, .coverage, and
coverage.xml but the file lacks a trailing newline; open the file
(python/packages/jumpstarter-driver-androidemulator/.gitignore) and ensure you
add a single POSIX-compliant newline character at the end of the file so the
last entry (coverage.xml) is terminated by a newline.
python/packages/jumpstarter-driver-androidemulator/examples/local-exporter.yaml (1)

1-15: LGTM! Correct emulator configuration.

The exporter config properly instantiates the AndroidEmulator driver with appropriate parameters. The omission of emulator_path is intentional and will use the default value. The headless: false setting is useful for local development and debugging. Port settings are consistent with Android emulator defaults and the ADB driver example.

Optional: Add trailing newline

Consider adding a trailing newline for POSIX text file compliance.

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

In
`@python/packages/jumpstarter-driver-androidemulator/examples/local-exporter.yaml`
around lines 1 - 15, Add a POSIX-compliant trailing newline to the YAML file by
ensuring the final line of the ExporterConfig (resource name "android-emulator"
/ kind ExporterConfig) ends with a newline character; simply update the file to
include a single newline at EOF so the file ends with a blank line.
python/packages/jumpstarter-driver-adb/.gitignore (1)

1-3: LGTM! Standard Python ignore patterns.

The ignore patterns correctly exclude Python cache and coverage artifacts. Optionally, consider adding a trailing newline for POSIX compliance.

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

In `@python/packages/jumpstarter-driver-adb/.gitignore` around lines 1 - 3, The
.gitignore file includes standard patterns (__pycache__/, .coverage,
coverage.xml) but lacks a trailing newline; open the file named .gitignore in
the python/packages/jumpstarter-driver-adb package and add a single trailing
newline at EOF so the file ends with a newline character to satisfy POSIX
conventions while keeping the existing ignore patterns unchanged.
python/examples/android-emulator/pyproject.toml (1)

1-16: LGTM! Well-structured example package configuration.

The package metadata and dependencies are appropriate for an Android emulator testing example. The pytest configuration with live logging at INFO level will help with debugging. The use of the [python-api] extra for the androidemulator driver is a good practice.

Optional: Add trailing newline

Consider adding a trailing newline for POSIX text file compliance.

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

In `@python/examples/android-emulator/pyproject.toml` around lines 1 - 16, The
file ends without a POSIX-compliant trailing newline; open the pyproject.toml
that contains the [project] table (e.g., the entry with name =
"jumpstarter-example-android-emulator") and add a single newline character at
the end of the file so the file terminates with a trailing newline.
python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver_test.py (1)

32-39: Add a test for non-integer port validation.

driver.py validates both range and type; this file currently covers range only. Adding a non-int case will close an important config-validation branch.

Suggested test addition
 def test_invalid_port_too_high():
     with pytest.raises(ConfigurationError):
         AdbServer(port=70000)

+def test_invalid_port_type():
+    with pytest.raises(ConfigurationError, match="Port must be an integer"):
+        AdbServer(port="5037")

Based on learnings: Each driver package must include comprehensive tests in driver_test.py.

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

In `@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver_test.py`
around lines 32 - 39, Add a new pytest that asserts AdbServer rejects
non-integer ports: create a test function (e.g., test_invalid_port_non_integer)
that calls AdbServer(port="not-an-int") (or port=1.5) inside with
pytest.raises(ConfigurationError) to cover the type-check branch in the
AdbServer constructor/driver.py validation logic.
python/packages/jumpstarter-driver-androidemulator/examples/exporter.yaml (1)

13-13: Consider making the example headless by default.

Line 13 sets headless: false; this often fails on CI/remote Linux nodes without a display server. Consider defaulting to true and mentioning how to enable GUI mode when needed.

Suggested tweak
-      headless: false
+      headless: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-driver-androidemulator/examples/exporter.yaml` at
line 13, Update the example to make the emulator run headlessly by default:
change the headless setting in exporter.yaml from false to true (the `headless:`
key), and add a short note in the example/comments explaining how to enable GUI
mode when needed (e.g., set `headless: false` or provide DISPLAY/Xvfb
instructions) so CI/remote Linux users won’t fail on systems without a display
server.
python/examples/android-emulator/jumpstarter_example_android_emulator/conftest.py (1)

19-22: Harden teardown with a try/finally around power lifecycle.

If client.power.on() fails after partially starting the emulator, Line 22 won’t run today. Wrapping on/yield/off in try/finally makes cleanup more reliable.

Suggested refactor
 from jumpstarter.common.utils import serve
+from contextlib import suppress
@@
     with serve(driver) as client:
-        client.power.on()
-        yield client
-        client.power.off()
+        try:
+            client.power.on()
+            yield client
+        finally:
+            with suppress(Exception):
+                client.power.off()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/examples/android-emulator/jumpstarter_example_android_emulator/conftest.py`
around lines 19 - 22, The current serve(driver) context block calls
client.power.on(), yields the client, then calls client.power.off() but if
client.power.on() or anything before the finally fails the power.off() won’t
run; wrap the on/yield/off sequence in a try/finally so that after obtaining
client (inside the with serve(driver) as client: block) you call
client.power.on() then try: yield client finally: client.power.off() to
guarantee cleanup even on errors in client.power.on(), during yield, or
downstream; update the block around serve(driver) and the client.power lifecycle
(client.power.on, yield client, client.power.off) accordingly.
python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver.py (1)

154-162: Inconsistent ADB path resolution in off() method.

The off() method resolves adb path separately using shutil.which("adb"), while the parent already has an AdbServer child with a resolved adb_path. Consider reusing the already-resolved path for consistency.

♻️ Proposed fix
         # Try graceful shutdown via ADB
         try:
-            adb_path = shutil.which("adb") or "adb"
+            adb_child = self.parent.children["adb"]
+            adb_path = adb_child.adb_path
             subprocess.run(
                 [adb_path, "-s", f"emulator-{self.parent.console_port}", "emu", "kill"],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver.py`
around lines 154 - 162, The off() method currently calls shutil.which("adb")
locally; change it to reuse the already-resolved ADB path from the parent's
AdbServer instance (e.g. use self.parent.adb_server.adb_path) when constructing
the subprocess.run call, keeping the same env override for
"ANDROID_ADB_SERVER_PORT" and the same arguments (including
f"emulator-{self.parent.console_port}") so ADB invocation is consistent with the
rest of the codebase.
python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver.py (2)

90-107: Consider adding a timeout to subprocess.run for kill_server.

Same concern as start_server - the call could block indefinitely without a timeout.

♻️ Proposed fix
         try:
             result = subprocess.run(
                 [self.adb_path, "kill-server"],
                 check=True,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
                 text=True,
                 env=self._adb_env(),
+                timeout=30,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver.py`
around lines 90 - 107, The kill_server method calls subprocess.run without a
timeout which can block; add a timeout argument (consistent with the timeout
used in start_server) to the subprocess.run call inside kill_server and handle
subprocess.TimeoutExpired by catching it (in addition to
subprocess.CalledProcessError), logging an appropriate error via
self.logger.error (including the timeout info and self.port), and returning
self.port as before; update the subprocess.run invocation in kill_server (which
uses self.adb_path, "kill-server", env=self._adb_env()) to include the timeout
and ensure stderr/stdout handling remains unchanged.

69-88: Consider adding a timeout to subprocess.run for start_server.

If the ADB server hangs during startup, this call will block indefinitely. Adding a reasonable timeout (e.g., 30 seconds) would prevent the driver from getting stuck.

♻️ Proposed fix
         try:
             result = subprocess.run(
                 [self.adb_path, "start-server"],
                 check=True,
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
                 text=True,
                 env=self._adb_env(),
+                timeout=30,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver.py`
around lines 69 - 88, The start_server method calls subprocess.run without a
timeout which can hang; update the call in start_server to pass a reasonable
timeout (e.g., timeout=30) and add an except block for subprocess.TimeoutExpired
to log the timeout (using self.logger.error or self.logger.warning) and handle
cleanup if needed, then return the port as before; reference the subprocess.run
call in start_server and the subprocess.TimeoutExpired exception to implement
the new timeout and error handling.
python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/client.py (1)

41-54: Consider adding type hint for the adb parameter.

The adb parameter lacks a type hint, which reduces code clarity. Since adbutils is an optional dependency, you could use a string annotation or Any.

♻️ Proposed fix
-    def _wait_for_boot(self, adb, timeout: int = 180) -> None:
+    def _wait_for_boot(self, adb: "adbutils.AdbClient", timeout: int = 180) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/client.py`
around lines 41 - 54, The _wait_for_boot method's adb parameter lacks a type
hint; update the signature of _wait_for_boot to annotate adb (e.g., adb: Any or
a string forward-reference like 'adbutils.AdbClient') and add the corresponding
import from typing (Any) at the top of the file so static checkers and readers
know this is an external/optional dependency; ensure the rest of the function
uses the same parameter name (_wait_for_boot(self, adb: Any, timeout: int = 180)
-> None).
python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/client.py (1)

27-50: Potential race condition in tunnel state file operations.

Multiple processes could race between reading and writing the tunnel state file. While unlikely in typical usage, consider using file locking or atomic writes for robustness.

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

In `@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/client.py`
around lines 27 - 50, The tunnel state file read/write/remove functions
(_read_tunnel_state, _write_tunnel_state, _remove_tunnel_state) are vulnerable
to races; fix them by using atomic file writes and OS-level file locks: when
writing in _write_tunnel_state, write to a temp file and atomically replace
_TUNNEL_STATE_FILE (os.replace) and acquire an exclusive lock (fcntl.flock or a
cross-platform locker) while writing; when reading in _read_tunnel_state,
acquire a shared lock before opening/parsing the JSON and before verifying the
pid to ensure consistency; similarly acquire an exclusive lock in
_remove_tunnel_state before unlinking. Ensure locks are released in finally
blocks and keep JSON port stored as int.
python/examples/android-emulator/setup.sh (1)

78-87: AVD existence check could have false positives.

The grep pattern "Name: $AVD_NAME" might match partial AVD names (e.g., jumpstarter_test_old when checking for jumpstarter_test). Consider using word boundaries or exact matching.

♻️ Proposed fix for more precise matching
-if ! avdmanager list avd 2>/dev/null | grep -q "Name: $AVD_NAME"; then
+if ! avdmanager list avd 2>/dev/null | grep -q "Name: ${AVD_NAME}$"; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/examples/android-emulator/setup.sh` around lines 78 - 87, The current
AVD existence check using grep "Name: $AVD_NAME" can match substrings and yield
false positives; update the check around avdmanager list avd to perform an exact
line match for the AVD name (e.g., match the "Name:" line exactly for AVD_NAME
using start/end anchors and optional whitespace) so only the precise AVD name is
detected; modify the grep invocation used with avdmanager list avd and the
AVD_NAME variable to use a regex like ^Name:[[:space:]]*<AVD_NAME>$ (or an
equivalent exact-match approach) to avoid partial matches.
python/examples/android-emulator/jumpstarter_example_android_emulator/test_android_emulator.py (1)

31-36: Consider a more robust wait for activity launch.

The fixed time.sleep(2) may be flaky on slower emulators. Consider polling dumpsys in a loop with a timeout, similar to the boot-wait pattern used elsewhere.

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

In
`@python/examples/android-emulator/jumpstarter_example_android_emulator/test_android_emulator.py`
around lines 31 - 36, Replace the fixed time.sleep(2) in test_launch_settings
with a polling loop (following the boot-wait pattern) that repeatedly calls
adb_device.shell("dumpsys activity activities") and checks for "settings" in the
output (case-insensitive) until a configurable timeout (e.g., 20–30s) is
reached; if the timeout expires, fail the test with an assertion or exception.
Locate the logic in the test_launch_settings function and use adb_device as the
source of dumpsys output, sleeping briefly (e.g., 0.5s) between polls to avoid
tight spinning.
🤖 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-adb/jumpstarter_driver_adb/client.py`:
- Around line 150-169: The tunnel branch currently reports an existing tunnel
without comparing requested CLI host/port; update the logic in the args[0] ==
"tunnel" block to read the requested host and port (the variables passed into
forward_adb) and compare them to the existing state returned by
_read_tunnel_state(); if state exists and matches (compare host strings and port
as int/str as needed) print the "already running" message and return 0, but if
state exists and does not match, print a clear error saying the requested
host/port are in use (include state['host'] and state['port']) and return a
non-zero status instead of silently claiming success; use the same symbols
forward_adb, _read_tunnel_state, _write_tunnel_state, and _remove_tunnel_state
to locate and implement the checks.

In `@python/packages/jumpstarter-driver-adb/README.md`:
- Around line 163-164: Fix the typo in the README sentence that currently reads
"You can also preform interactions via ADB using the
[`adbutils`](https://github.com/openatx/adbutils) Python package." — change
"preform" to "perform" so the sentence correctly reads "You can also perform
interactions via ADB using the [`adbutils`](https://github.com/openatx/adbutils)
Python package." Ensure this edit is applied in the README.md near the adbutils
mention.

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py`:
- Around line 13-15: Tests patch the global shutil.which for AdbServer usage;
change those `@patch` decorators to target the module where AdbServer imports
shutil (use jumpstarter_driver_adb.driver.shutil.which) so the patch applies to
the module-scoped symbol. Update every similar decorator that currently patches
"shutil.which" in this test file (affecting tests that exercise AdbServer and
related functions) to patch "jumpstarter_driver_adb.driver.shutil.which"
instead, keeping the same return_value and order as the other patches.

---

Nitpick comments:
In
`@python/examples/android-emulator/jumpstarter_example_android_emulator/conftest.py`:
- Around line 19-22: The current serve(driver) context block calls
client.power.on(), yields the client, then calls client.power.off() but if
client.power.on() or anything before the finally fails the power.off() won’t
run; wrap the on/yield/off sequence in a try/finally so that after obtaining
client (inside the with serve(driver) as client: block) you call
client.power.on() then try: yield client finally: client.power.off() to
guarantee cleanup even on errors in client.power.on(), during yield, or
downstream; update the block around serve(driver) and the client.power lifecycle
(client.power.on, yield client, client.power.off) accordingly.

In
`@python/examples/android-emulator/jumpstarter_example_android_emulator/test_android_emulator.py`:
- Around line 31-36: Replace the fixed time.sleep(2) in test_launch_settings
with a polling loop (following the boot-wait pattern) that repeatedly calls
adb_device.shell("dumpsys activity activities") and checks for "settings" in the
output (case-insensitive) until a configurable timeout (e.g., 20–30s) is
reached; if the timeout expires, fail the test with an assertion or exception.
Locate the logic in the test_launch_settings function and use adb_device as the
source of dumpsys output, sleeping briefly (e.g., 0.5s) between polls to avoid
tight spinning.

In `@python/examples/android-emulator/pyproject.toml`:
- Around line 1-16: The file ends without a POSIX-compliant trailing newline;
open the pyproject.toml that contains the [project] table (e.g., the entry with
name = "jumpstarter-example-android-emulator") and add a single newline
character at the end of the file so the file terminates with a trailing newline.

In `@python/examples/android-emulator/setup.sh`:
- Around line 78-87: The current AVD existence check using grep "Name:
$AVD_NAME" can match substrings and yield false positives; update the check
around avdmanager list avd to perform an exact line match for the AVD name
(e.g., match the "Name:" line exactly for AVD_NAME using start/end anchors and
optional whitespace) so only the precise AVD name is detected; modify the grep
invocation used with avdmanager list avd and the AVD_NAME variable to use a
regex like ^Name:[[:space:]]*<AVD_NAME>$ (or an equivalent exact-match approach)
to avoid partial matches.

In `@python/packages/jumpstarter-driver-adb/.gitignore`:
- Around line 1-3: The .gitignore file includes standard patterns (__pycache__/,
.coverage, coverage.xml) but lacks a trailing newline; open the file named
.gitignore in the python/packages/jumpstarter-driver-adb package and add a
single trailing newline at EOF so the file ends with a newline character to
satisfy POSIX conventions while keeping the existing ignore patterns unchanged.

In `@python/packages/jumpstarter-driver-adb/examples/exporter.yaml`:
- Around line 1-19: Add a POSIX-compliant trailing newline to the YAML example
for the ExporterConfig named "adb-local" (apiVersion jumpstarter.dev/v1alpha1,
kind ExporterConfig) by ensuring the file ends with a single newline character
after the final "port: 15037" line; this is a purely formatting change to the
examples/exporter.yaml content so text tools and linters recognize it as a POSIX
text file.

In `@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/client.py`:
- Around line 27-50: The tunnel state file read/write/remove functions
(_read_tunnel_state, _write_tunnel_state, _remove_tunnel_state) are vulnerable
to races; fix them by using atomic file writes and OS-level file locks: when
writing in _write_tunnel_state, write to a temp file and atomically replace
_TUNNEL_STATE_FILE (os.replace) and acquire an exclusive lock (fcntl.flock or a
cross-platform locker) while writing; when reading in _read_tunnel_state,
acquire a shared lock before opening/parsing the JSON and before verifying the
pid to ensure consistency; similarly acquire an exclusive lock in
_remove_tunnel_state before unlinking. Ensure locks are released in finally
blocks and keep JSON port stored as int.

In
`@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver_test.py`:
- Around line 32-39: Add a new pytest that asserts AdbServer rejects non-integer
ports: create a test function (e.g., test_invalid_port_non_integer) that calls
AdbServer(port="not-an-int") (or port=1.5) inside with
pytest.raises(ConfigurationError) to cover the type-check branch in the
AdbServer constructor/driver.py validation logic.

In `@python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver.py`:
- Around line 90-107: The kill_server method calls subprocess.run without a
timeout which can block; add a timeout argument (consistent with the timeout
used in start_server) to the subprocess.run call inside kill_server and handle
subprocess.TimeoutExpired by catching it (in addition to
subprocess.CalledProcessError), logging an appropriate error via
self.logger.error (including the timeout info and self.port), and returning
self.port as before; update the subprocess.run invocation in kill_server (which
uses self.adb_path, "kill-server", env=self._adb_env()) to include the timeout
and ensure stderr/stdout handling remains unchanged.
- Around line 69-88: The start_server method calls subprocess.run without a
timeout which can hang; update the call in start_server to pass a reasonable
timeout (e.g., timeout=30) and add an except block for subprocess.TimeoutExpired
to log the timeout (using self.logger.error or self.logger.warning) and handle
cleanup if needed, then return the port as before; reference the subprocess.run
call in start_server and the subprocess.TimeoutExpired exception to implement
the new timeout and error handling.

In `@python/packages/jumpstarter-driver-androidemulator/.gitignore`:
- Around line 1-3: The .gitignore contents correctly list __pycache__/,
.coverage, and coverage.xml but the file lacks a trailing newline; open the file
(python/packages/jumpstarter-driver-androidemulator/.gitignore) and ensure you
add a single POSIX-compliant newline character at the end of the file so the
last entry (coverage.xml) is terminated by a newline.

In `@python/packages/jumpstarter-driver-androidemulator/examples/exporter.yaml`:
- Line 13: Update the example to make the emulator run headlessly by default:
change the headless setting in exporter.yaml from false to true (the `headless:`
key), and add a short note in the example/comments explaining how to enable GUI
mode when needed (e.g., set `headless: false` or provide DISPLAY/Xvfb
instructions) so CI/remote Linux users won’t fail on systems without a display
server.

In
`@python/packages/jumpstarter-driver-androidemulator/examples/local-exporter.yaml`:
- Around line 1-15: Add a POSIX-compliant trailing newline to the YAML file by
ensuring the final line of the ExporterConfig (resource name "android-emulator"
/ kind ExporterConfig) ends with a newline character; simply update the file to
include a single newline at EOF so the file ends with a blank line.

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/client.py`:
- Around line 41-54: The _wait_for_boot method's adb parameter lacks a type
hint; update the signature of _wait_for_boot to annotate adb (e.g., adb: Any or
a string forward-reference like 'adbutils.AdbClient') and add the corresponding
import from typing (Any) at the top of the file so static checkers and readers
know this is an external/optional dependency; ensure the rest of the function
uses the same parameter name (_wait_for_boot(self, adb: Any, timeout: int = 180)
-> None).

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver.py`:
- Around line 154-162: The off() method currently calls shutil.which("adb")
locally; change it to reuse the already-resolved ADB path from the parent's
AdbServer instance (e.g. use self.parent.adb_server.adb_path) when constructing
the subprocess.run call, keeping the same env override for
"ANDROID_ADB_SERVER_PORT" and the same arguments (including
f"emulator-{self.parent.console_port}") so ADB invocation is consistent with the
rest of the codebase.
🪄 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: bcc1f39e-ac4c-4741-aa5c-941074321a4a

📥 Commits

Reviewing files that changed from the base of the PR and between b21e591 and b1d8a6e.

⛔ Files ignored due to path filters (1)
  • python/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • python/docs/source/reference/package-apis/drivers/adb.md
  • python/docs/source/reference/package-apis/drivers/androidemulator.md
  • python/docs/source/reference/package-apis/drivers/index.md
  • python/examples/android-emulator/README.md
  • python/examples/android-emulator/jumpstarter_example_android_emulator/__init__.py
  • python/examples/android-emulator/jumpstarter_example_android_emulator/conftest.py
  • python/examples/android-emulator/jumpstarter_example_android_emulator/test_android_emulator.py
  • python/examples/android-emulator/pyproject.toml
  • python/examples/android-emulator/setup.sh
  • python/packages/jumpstarter-driver-adb/.gitignore
  • python/packages/jumpstarter-driver-adb/README.md
  • python/packages/jumpstarter-driver-adb/examples/exporter.yaml
  • python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/__init__.py
  • python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/client.py
  • python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver.py
  • python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/driver_test.py
  • python/packages/jumpstarter-driver-adb/pyproject.toml
  • python/packages/jumpstarter-driver-androidemulator/.gitignore
  • python/packages/jumpstarter-driver-androidemulator/README.md
  • python/packages/jumpstarter-driver-androidemulator/examples/exporter.yaml
  • python/packages/jumpstarter-driver-androidemulator/examples/local-exporter.yaml
  • python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/__init__.py
  • python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/client.py
  • python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver.py
  • python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py
  • python/packages/jumpstarter-driver-androidemulator/pyproject.toml
  • python/pyproject.toml

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 (3)
python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py (3)

32-37: Consider adding test coverage for adb_server_port validation.

The driver validates both console_port and adb_server_port in the same loop (per context snippet 2), but only console_port validation is tested here. Adding a test for invalid adb_server_port (e.g., -1 or 65536) would improve coverage.

📝 Example additional test
`@patch`("jumpstarter_driver_androidemulator.driver.shutil.which", return_value="/usr/bin/emulator")
`@patch`("jumpstarter_driver_adb.driver.shutil.which", return_value="/usr/bin/adb")
`@patch`("subprocess.run", return_value=_mock_adb_ok())
def test_init_invalid_adb_server_port(mock_run, mock_adb_which, mock_emu_which):
    with pytest.raises(ConfigurationError, match="Invalid adb_server_port"):
        AndroidEmulator(avd_name="test_avd", adb_server_port=-1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py`
around lines 32 - 37, Add a parallel test that asserts invalid adb_server_port
values raise ConfigurationError: create a new test (mirroring
test_init_invalid_port) that patches
jumpstarter_driver_androidemulator.driver.shutil.which and
jumpstarter_driver_adb.driver.shutil.which and subprocess.run, then calls
AndroidEmulator(avd_name="test_avd", adb_server_port=-1) inside
pytest.raises(ConfigurationError, match="Invalid adb_server_port"); this ensures
the adb_server_port validation path in AndroidEmulator is covered.

15-15: Consider module-scoped patch for subprocess.run for consistency.

The shutil.which patches correctly target their respective modules, but subprocess.run is patched globally. For consistency and to avoid potential issues in more complex test scenarios, consider using jumpstarter_driver_adb.driver.subprocess.run (where AdbServer's __post_init__ likely calls it for validation).

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

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py`
at line 15, The global patch of subprocess.run should be changed to a
module-scoped patch so it mocks the call used by AdbServer; update any
`@patch`("subprocess.run", ...) decorators in driver_test.py to target the driver
module's subprocess (e.g.,
`@patch`("jumpstarter_driver_adb.driver.subprocess.run",
return_value=_mock_adb_ok())) so that AdbServer.__post_init__ and other calls
within jumpstarter_driver_adb.driver use the mocked subprocess.run; keep the
same return_value and behavior but change only the patch target string.

115-115: Consider moving TimeoutExpired import to module level.

The from subprocess import TimeoutExpired import is placed inside the test function. Moving it to the module-level imports would follow standard Python conventions and make the import visible at a glance.

📝 Suggested change
-from unittest.mock import MagicMock, patch
+from subprocess import TimeoutExpired
+from unittest.mock import MagicMock, patch

Then remove the import from line 115.

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

In
`@python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py`
at line 115, Move the `from subprocess import TimeoutExpired` import from inside
the test function to the module-level imports at the top of driver_test.py and
remove the in-function import; update the existing top import block to include
`TimeoutExpired` so references in the test (the test that currently contains the
inline import) use the module-level symbol instead.
🤖 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-androidemulator/jumpstarter_driver_androidemulator/driver_test.py`:
- Around line 32-37: Add a parallel test that asserts invalid adb_server_port
values raise ConfigurationError: create a new test (mirroring
test_init_invalid_port) that patches
jumpstarter_driver_androidemulator.driver.shutil.which and
jumpstarter_driver_adb.driver.shutil.which and subprocess.run, then calls
AndroidEmulator(avd_name="test_avd", adb_server_port=-1) inside
pytest.raises(ConfigurationError, match="Invalid adb_server_port"); this ensures
the adb_server_port validation path in AndroidEmulator is covered.
- Line 15: The global patch of subprocess.run should be changed to a
module-scoped patch so it mocks the call used by AdbServer; update any
`@patch`("subprocess.run", ...) decorators in driver_test.py to target the driver
module's subprocess (e.g.,
`@patch`("jumpstarter_driver_adb.driver.subprocess.run",
return_value=_mock_adb_ok())) so that AdbServer.__post_init__ and other calls
within jumpstarter_driver_adb.driver use the mocked subprocess.run; keep the
same return_value and behavior but change only the patch target string.
- Line 115: Move the `from subprocess import TimeoutExpired` import from inside
the test function to the module-level imports at the top of driver_test.py and
remove the in-function import; update the existing top import block to include
`TimeoutExpired` so references in the test (the test that currently contains the
inline import) use the module-level symbol instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3fd439e-2878-4564-9719-215f49252759

📥 Commits

Reviewing files that changed from the base of the PR and between b1d8a6e and eef5a87.

📒 Files selected for processing (3)
  • python/packages/jumpstarter-driver-adb/README.md
  • python/packages/jumpstarter-driver-adb/jumpstarter_driver_adb/client.py
  • python/packages/jumpstarter-driver-androidemulator/jumpstarter_driver_androidemulator/driver_test.py

]

[project.optional-dependencies]
python-api = ["adbutils>=2.8.7"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would perhaps even make it mandatory unless adbutils is too huge

Copy link
Copy Markdown
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

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

It looks great, let's bump that dependency to mandatory in a later PR :)

@mangelajo mangelajo merged commit fba1b25 into main Apr 6, 2026
31 checks passed
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.

2 participants