Skip to content
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
1 change: 1 addition & 0 deletions python/docs/source/reference/package-apis/drivers/adb.md
6 changes: 6 additions & 0 deletions python/docs/source/reference/package-apis/drivers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Drivers that control the power state and basic operation of devices:

Drivers that provide various communication interfaces:

* **[ADB](adb.md)** (`jumpstarter-driver-adb`) - Android Debug Bridge tunneling
for remote Android device access
* **[BLE](ble.md)** (`jumpstarter-driver-ble`) - Bluetooth Low Energy communication
* **[CAN](can.md)** (`jumpstarter-driver-can`) - Controller Area Network
communication
Expand Down Expand Up @@ -88,6 +90,8 @@ Drivers for debugging and programming devices:
UF2 flashing via BOOTSEL mass storage
* **[Probe-RS](probe-rs.md)** (`jumpstarter-driver-probe-rs`) - Debugging probe
support
* **[Android Emulator](androidemulator.md)** (`jumpstarter-driver-androidemulator`) -
Android emulator lifecycle management with ADB tunneling
* **[QEMU](qemu.md)** (`jumpstarter-driver-qemu`) - QEMU virtualization platform
* **[Corellium](corellium.md)** (`jumpstarter-driver-corellium`) - Corellium
virtualization platform
Expand All @@ -105,6 +109,8 @@ General-purpose utility drivers:

```{toctree}
:hidden:
adb.md
androidemulator.md
ble.md
can.md
corellium.md
Expand Down
69 changes: 69 additions & 0 deletions python/examples/android-emulator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Android Emulator Testing Example

This example demonstrates testing an Android device through Jumpstarter's ADB
tunneling. It boots an Android emulator, connects via ADB through the
Jumpstarter gRPC pipeline, and runs tests using the `adbutils` Python API.

## Prerequisites

- Android SDK with emulator and platform-tools
- An Android Virtual Device (AVD) created

## Quick Setup

The `setup.sh` script handles SDK detection, system image installation, and AVD
creation automatically:

```bash
cd python/examples/android-emulator
source setup.sh
```

This will:

1. Find your Android SDK (checks `ANDROID_HOME`, then common default locations)
2. Detect your CPU architecture (arm64 for Apple Silicon, x86_64 for Intel)
3. Install the appropriate system image if not present
4. Create a `jumpstarter_test` AVD if it doesn't exist
5. Set `PATH` and `ANDROID_AVD_NAME` environment variables

## Running the Tests

```bash
pytest jumpstarter_example_android_emulator/ -v
```

Or with a custom AVD:

```bash
ANDROID_AVD_NAME=my_avd pytest jumpstarter_example_android_emulator/ -v
```

## What the Tests Demonstrate

All tests run through the full Jumpstarter driver/gRPC/client pipeline:

| Test | Description |
| ------------------------ | --------------------------------------------------- |
| `test_device_properties` | Read device model and SDK version via `getprop` |
| `test_list_packages` | List installed packages, verify Settings app exists |
| `test_launch_settings` | Launch built-in Settings activity |
| `test_file_push_pull` | Push/pull files to/from the device |
| `test_display_size` | Read display dimensions |
| `test_battery_info` | Read battery status from `dumpsys` |
| `test_adb_list_devices` | Verify ADB server on exporter sees the emulator |

## Architecture

```text
[Test Code] -> [AndroidEmulatorClient] -> [gRPC] -> [AndroidEmulator Driver]
| |
|-- .power.on/off() |-- AndroidEmulatorPower (emulator process)
|-- .adb.forward_adb() |-- AdbServer (port 15037)
|-- .adb_device() |
v v
[adbutils] <-- TCP tunnel --> [ADB Server] <-- [Emulator]
```

The emulator boots once (session-scoped fixture) and all tests share the
connection, keeping total run time reasonable despite the ~60 second boot time.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os

import pytest
from jumpstarter_driver_androidemulator.driver import AndroidEmulator

from jumpstarter.common.utils import serve


@pytest.fixture(scope="session")
def emulator_client():
"""Boot an Android emulator and yield a Jumpstarter client.

The emulator runs for the entire test session and is shut down
at the end. Set ANDROID_AVD_NAME to specify which AVD to use.
"""
avd_name = os.environ.get("ANDROID_AVD_NAME", "jumpstarter_test")
driver = AndroidEmulator(avd_name=avd_name, headless=False)

with serve(driver) as client:
client.power.on()
yield client
client.power.off()


@pytest.fixture(scope="session")
def adb_device(emulator_client):
"""Yield an adbutils device connected through the ADB tunnel.

Waits for the emulator to finish booting before yielding.
"""
with emulator_client.adb_device(timeout=180) as device:
yield device
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Android emulator integration tests using Jumpstarter ADB tunneling.

These tests demonstrate interacting with an Android device through
the Jumpstarter ADB tunnel using the adbutils Python API. No APK
is required — all tests use built-in Android capabilities.
"""

import os
import tempfile
import time


def test_device_properties(adb_device):
"""Read basic device properties via getprop."""
model = adb_device.prop.model
assert model is not None and len(model) > 0

sdk = adb_device.prop.get("ro.build.version.sdk")
assert sdk is not None
assert int(sdk) > 0


def test_list_packages(adb_device):
"""Verify standard system packages are installed."""
output = adb_device.shell("pm list packages")
packages = output.strip().split("\n")
assert len(packages) > 0
assert any("com.android.settings" in p for p in packages)


def test_launch_settings(adb_device):
"""Launch the Settings app and verify it starts."""
adb_device.shell("am start -a android.settings.SETTINGS")
time.sleep(2)
output = adb_device.shell("dumpsys activity activities")
assert "settings" in output.lower()


def test_file_push_pull(adb_device):
"""Push a file to the device and pull it back."""
test_content = b"jumpstarter-adb-test-content"
remote_path = "/data/local/tmp/jumpstarter_test.txt"

with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as f:
f.write(test_content)
local_path = f.name

pull_path = local_path + ".pulled"
try:
adb_device.sync.push(local_path, remote_path)

result = adb_device.shell(f"cat {remote_path}")
assert result.strip() == test_content.decode()

adb_device.sync.pull(remote_path, pull_path)
with open(pull_path, "rb") as f:
assert f.read() == test_content

adb_device.shell(f"rm {remote_path}")
finally:
os.unlink(local_path)
if os.path.exists(pull_path):
os.unlink(pull_path)


def test_display_size(adb_device):
"""Read the display size."""
output = adb_device.shell("wm size")
assert "x" in output


def test_battery_info(adb_device):
"""Read battery information from the emulator."""
output = adb_device.shell("dumpsys battery")
assert "level" in output.lower()
assert "status" in output.lower()


def test_adb_list_devices(emulator_client):
"""Verify the exporter's ADB server sees the emulator."""
output = emulator_client.adb.list_devices()
assert "emulator" in output or "device" in output
16 changes: 16 additions & 0 deletions python/examples/android-emulator/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[project]
name = "jumpstarter-example-android-emulator"
version = "0.1.0"
description = "Android emulator testing example using Jumpstarter ADB tunneling"
authors = [{ name = "Kirk Brauer", email = "kbrauer@hatci.com" }]
license = "Apache-2.0"
requires-python = ">=3.11"
dependencies = [
"pytest>=8.3.2",
"jumpstarter",
"jumpstarter-driver-androidemulator[python-api]",
]

[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"
98 changes: 98 additions & 0 deletions python/examples/android-emulator/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
# Setup script for Android emulator testing example.
# Locates the Android SDK, installs a system image, and creates a test AVD.
#
# Usage: source setup.sh
#
# This script should be sourced (not executed) so that PATH and
# environment variables are set in the current shell.

set -e

AVD_NAME="${ANDROID_AVD_NAME:-jumpstarter_test}"
DEVICE="pixel_6"

# --- Locate Android SDK ---
if [ -z "$ANDROID_HOME" ]; then
# Common default locations
for candidate in \
"$HOME/Library/Android/sdk" \
"$HOME/Android/Sdk" \
"/opt/android-sdk"; do
if [ -d "$candidate" ]; then
export ANDROID_HOME="$candidate"
break
fi
done
fi

if [ -z "$ANDROID_HOME" ] || [ ! -d "$ANDROID_HOME" ]; then
echo "ERROR: Android SDK not found."
echo " Install Android Studio or set ANDROID_HOME to your SDK path."
return 1 2>/dev/null || exit 1
fi

echo "Android SDK: $ANDROID_HOME"

# --- Add tools to PATH ---
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"

# --- Verify required tools ---
for tool in emulator adb sdkmanager avdmanager; do
if ! command -v "$tool" &>/dev/null; then
echo "ERROR: '$tool' not found in PATH."
echo " Ensure Android SDK command-line tools are installed."
return 1 2>/dev/null || exit 1
fi
done

# --- Detect architecture ---
ARCH=$(uname -m)
case "$ARCH" in
arm64|aarch64)
SYS_IMAGE_ARCH="arm64-v8a"
;;
x86_64|amd64)
SYS_IMAGE_ARCH="x86_64"
;;
*)
echo "ERROR: Unsupported architecture: $ARCH"
return 1 2>/dev/null || exit 1
;;
esac

SYS_IMAGE="system-images;android-35;google_apis;${SYS_IMAGE_ARCH}"
echo "Architecture: $ARCH -> $SYS_IMAGE_ARCH"
echo "System image: $SYS_IMAGE"

# --- Install system image if missing ---
SYS_IMAGE_PATH="$ANDROID_HOME/system-images/android-35/google_apis/${SYS_IMAGE_ARCH}"
if [ ! -d "$SYS_IMAGE_PATH" ]; then
echo "Installing system image (this may take a few minutes)..."
yes | sdkmanager "$SYS_IMAGE"
else
echo "System image already installed at $SYS_IMAGE_PATH"
fi

# --- Create AVD if missing ---
if ! avdmanager list avd 2>/dev/null | grep -q "Name: $AVD_NAME"; then
echo "Creating AVD: $AVD_NAME (device: $DEVICE)..."
echo "no" | avdmanager create avd \
-n "$AVD_NAME" \
-k "$SYS_IMAGE" \
-d "$DEVICE" \
--force
else
echo "AVD '$AVD_NAME' already exists."
fi

# --- Export for tests ---
export ANDROID_AVD_NAME="$AVD_NAME"

echo ""
echo "Setup complete. To run the tests:"
echo " pytest jumpstarter_example_android_emulator/ -v"
echo ""
echo "Environment:"
echo " ANDROID_HOME=$ANDROID_HOME"
echo " ANDROID_AVD_NAME=$ANDROID_AVD_NAME"
3 changes: 3 additions & 0 deletions python/packages/jumpstarter-driver-adb/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
.coverage
coverage.xml
Loading
Loading