mcp-adb/tests/test_devices.py
Ryan Malloy fb297f7937 Add pytest suite (216 tests) and fix UI/notification parser bugs
Test infrastructure with conftest fixtures mocking run_shell_args/run_adb
for device-free testing across all 8 mixins.

Fixed: UI parser regex couldn't match hyphenated XML attributes
(content-desc, resource-id). Notification parser captured trailing
parenthesis in package names.
2026-02-11 03:38:37 -07:00

182 lines
6.7 KiB
Python

"""Tests for devices mixin (list, use, current, info, reboot, logcat)."""
import pytest
from tests.conftest import fail, ok
class TestDevicesList:
async def test_parse_devices(self, server):
server.run_adb.return_value = ok(
stdout=(
"List of devices attached\n"
"ABC123\tdevice\tmodel:Pixel_6 product:oriole\n"
"10.20.0.25:5555\tdevice\tmodel:K2401 product:K2401\n"
)
)
devices = await server.devices_list()
assert len(devices) == 2
assert devices[0].device_id == "ABC123"
assert devices[0].model == "Pixel_6"
assert devices[1].device_id == "10.20.0.25:5555"
async def test_empty(self, server):
server.run_adb.return_value = ok(stdout="List of devices attached\n")
devices = await server.devices_list()
assert len(devices) == 0
async def test_failure(self, server):
server.run_adb.return_value = fail("adb not found")
devices = await server.devices_list()
assert len(devices) == 0
class TestDevicesUse:
async def test_select_device(self, server):
server.run_adb.return_value = ok(
stdout="List of devices attached\nABC123\tdevice\n"
)
result = await server.devices_use("ABC123")
assert result["success"] is True
assert server.get_current_device() == "ABC123"
async def test_not_found(self, server):
server.run_adb.return_value = ok(
stdout="List of devices attached\nOTHER\tdevice\n"
)
result = await server.devices_use("MISSING")
assert result["success"] is False
assert "not found" in result["error"]
async def test_offline_device(self, server):
server.run_adb.return_value = ok(
stdout="List of devices attached\nABC123\toffline\n"
)
result = await server.devices_use("ABC123")
assert result["success"] is False
assert "offline" in result["error"]
class TestDevicesCurrent:
async def test_no_device_set(self, server):
server.run_adb.return_value = ok(stdout="List of devices attached\n")
result = await server.devices_current()
assert result["device"] is None
async def test_auto_detect_single(self, server):
server.run_adb.return_value = ok(
stdout="List of devices attached\nABC123\tdevice\n"
)
result = await server.devices_current()
assert result.get("available") is not None
async def test_device_set(self, server):
# Pre-populate cache and set device
server.run_adb.return_value = ok(
stdout="List of devices attached\nABC123\tdevice\tmodel:Pixel\n"
)
await server.devices_list()
server.set_current_device("ABC123")
result = await server.devices_current()
assert result["device"]["device_id"] == "ABC123"
class TestDeviceInfo:
async def test_full_info(self, server):
battery = (
"Current Battery Service state:\n level: 85\n status: 2\n plugged: 2"
)
df_out = (
"Filesystem 1K-blocks Used Available\n"
"/data 64000000 32000000 32000000"
)
server.run_shell_args.side_effect = [
ok(stdout=battery),
ok(stdout="10: wlan0 inet 192.168.1.100/24"),
ok(stdout="mWifiInfo SSID: MyNetwork, BSSID: ..."),
ok(stdout=df_out),
]
server.get_device_property.side_effect = lambda p, d=None: {
"ro.build.version.release": "14",
"ro.build.version.sdk": "34",
"ro.product.model": "Pixel 6",
"ro.product.manufacturer": "Google",
"ro.product.device": "oriole",
}.get(p)
result = await server.device_info()
assert result["success"] is True
assert result["battery"]["level"] == 85
assert result["ip_address"] == "192.168.1.100"
assert result["wifi_ssid"] == "MyNetwork"
assert result["model"] == "Pixel 6"
async def test_device_offline(self, server):
server.run_shell_args.return_value = fail("device offline")
result = await server.device_info()
assert result["success"] is False
class TestDeviceReboot:
@pytest.mark.usefixtures("_dev_mode")
async def test_reboot(self, server, ctx):
ctx.set_elicit("accept", "Yes, reboot now")
server.run_adb.return_value = ok()
result = await server.device_reboot(ctx)
assert result["success"] is True
assert result["mode"] == "normal"
@pytest.mark.usefixtures("_dev_mode")
async def test_reboot_recovery(self, server, ctx):
ctx.set_elicit("accept", "Yes, reboot now")
server.run_adb.return_value = ok()
result = await server.device_reboot(ctx, mode="recovery")
assert result["mode"] == "recovery"
@pytest.mark.usefixtures("_dev_mode")
async def test_cancelled(self, server, ctx):
ctx.set_elicit("accept", "Cancel")
result = await server.device_reboot(ctx)
assert result["success"] is False
assert result.get("cancelled") is True
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server, ctx):
result = await server.device_reboot(ctx)
assert result["success"] is False
class TestLogcat:
@pytest.mark.usefixtures("_dev_mode")
async def test_capture(self, server):
logline = "01-01 00:00:00.000 I/TAG: message"
server.run_shell_args.return_value = ok(stdout=logline)
result = await server.logcat_capture()
assert result["success"] is True
assert result["output"].startswith("01-01")
@pytest.mark.usefixtures("_dev_mode")
async def test_with_filter(self, server):
server.run_shell_args.return_value = ok(stdout="filtered output")
result = await server.logcat_capture(filter_spec="MyApp:D *:S")
assert result["filter"] == "MyApp:D *:S"
@pytest.mark.usefixtures("_dev_mode")
async def test_clear_first(self, server):
server.run_shell_args.side_effect = [ok(), ok(stdout="fresh logs")]
result = await server.logcat_capture(clear_first=True)
assert result["success"] is True
assert server.run_shell_args.call_count == 2
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.logcat_capture()
assert result["success"] is False
@pytest.mark.usefixtures("_dev_mode")
async def test_logcat_clear(self, server):
server.run_shell_args.return_value = ok()
result = await server.logcat_clear()
assert result["success"] is True
assert result["action"] == "logcat_clear"