274 lines
11 KiB
Python
274 lines
11 KiB
Python
"""ADB 设备边界测试:所有命令执行器均为 mock,不连接真机。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
|
|
|
|
CLIENT_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(CLIENT_ROOT / "src"))
|
|
|
|
from cmbuyer_client.device.adb import (
|
|
AdbClient,
|
|
CommandResult,
|
|
DeviceIdentityUnconfirmedError,
|
|
DeviceCommandError,
|
|
DeviceCommandTimeoutError,
|
|
DeviceNotFoundError,
|
|
DeviceOfflineError,
|
|
DeviceStateError,
|
|
DeviceUnauthorizedError,
|
|
DuplicatePhysicalDeviceError,
|
|
IntentLaunchUnconfirmedError,
|
|
SerialRequiredError,
|
|
)
|
|
|
|
|
|
USB_SERIAL = "3B65BD02H7F00000"
|
|
WIFI_SERIAL = "192.168.0.173:5555"
|
|
|
|
|
|
class FakeRunner:
|
|
def __init__(self, devices_output: str, properties: dict[tuple[str, str], CommandResult | str]) -> None:
|
|
self.devices_output = devices_output
|
|
self.properties = properties
|
|
self.calls: list[tuple[str, ...]] = []
|
|
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
self.calls.append(tuple(arguments))
|
|
if tuple(arguments) == ("devices", "-l"):
|
|
return CommandResult(stdout=self.devices_output)
|
|
key = (arguments[1], arguments[-1])
|
|
value = self.properties.get(key, "")
|
|
return value if isinstance(value, CommandResult) else CommandResult(stdout=value)
|
|
|
|
|
|
def _properties(serials: tuple[str, ...]) -> dict[tuple[str, str], str]:
|
|
values: dict[tuple[str, str], str] = {}
|
|
for serial in serials:
|
|
values[(serial, "ro.serialno")] = "physical-phone-1"
|
|
values[(serial, "ro.boot.serialno")] = "physical-phone-1"
|
|
values[(serial, "ro.product.model")] = "PKG110"
|
|
values[(serial, "ro.product.name")] = "PKG110"
|
|
values[(serial, "ro.product.device")] = "OP5D2BL1"
|
|
values[(serial, "ro.build.version.release")] = "16"
|
|
return values
|
|
|
|
|
|
class AdbClientTests(unittest.TestCase):
|
|
def test_requires_explicit_serial(self) -> None:
|
|
runner = FakeRunner("List of devices attached\n", {})
|
|
|
|
with self.assertRaises(SerialRequiredError):
|
|
AdbClient(runner).inspect(" ")
|
|
|
|
self.assertEqual(runner.calls, [])
|
|
|
|
def test_missing_offline_and_unauthorized_are_distinct(self) -> None:
|
|
missing = AdbClient(FakeRunner("List of devices attached\n", {}))
|
|
with self.assertRaises(DeviceNotFoundError):
|
|
missing.inspect(USB_SERIAL)
|
|
|
|
offline = AdbClient(FakeRunner(f"List of devices attached\n{USB_SERIAL}\toffline\n", {}))
|
|
with self.assertRaises(DeviceOfflineError):
|
|
offline.inspect(USB_SERIAL)
|
|
|
|
unauthorized = AdbClient(FakeRunner(f"List of devices attached\n{USB_SERIAL}\tunauthorized\n", {}))
|
|
with self.assertRaises(DeviceUnauthorizedError):
|
|
unauthorized.inspect(USB_SERIAL)
|
|
|
|
def test_two_channels_with_same_physical_identity_fail_closed(self) -> None:
|
|
output = (
|
|
"List of devices attached\n"
|
|
f"{WIFI_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
f"{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
)
|
|
runner = FakeRunner(output, _properties((WIFI_SERIAL, USB_SERIAL)))
|
|
|
|
with self.assertRaises(DuplicatePhysicalDeviceError):
|
|
AdbClient(runner).inspect(USB_SERIAL)
|
|
|
|
self.assertIn(("-s", WIFI_SERIAL, "shell", "getprop", "ro.serialno"), runner.calls)
|
|
self.assertIn(("-s", USB_SERIAL, "shell", "getprop", "ro.serialno"), runner.calls)
|
|
|
|
def test_multiple_online_devices_with_failed_identity_fail_closed(self) -> None:
|
|
output = (
|
|
"List of devices attached\n"
|
|
f"{WIFI_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
f"{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
)
|
|
properties = _properties((WIFI_SERIAL, USB_SERIAL))
|
|
properties[(WIFI_SERIAL, "ro.serialno")] = CommandResult(stdout="", returncode=1)
|
|
runner = FakeRunner(output, properties)
|
|
|
|
with self.assertRaises(DeviceIdentityUnconfirmedError):
|
|
AdbClient(runner).inspect(USB_SERIAL)
|
|
|
|
def test_online_explicit_serial_reads_non_sensitive_metadata(self) -> None:
|
|
output = f"List of devices attached\n{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
inspection = AdbClient(FakeRunner(output, _properties((USB_SERIAL,)))).inspect(USB_SERIAL)
|
|
|
|
self.assertEqual(inspection.device.serial, USB_SERIAL)
|
|
self.assertEqual(inspection.model, "PKG110")
|
|
self.assertEqual(inspection.android_version, "16")
|
|
|
|
def test_single_online_device_does_not_require_hardware_identity(self) -> None:
|
|
output = f"List of devices attached\n{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
properties = _properties((USB_SERIAL,))
|
|
properties[(USB_SERIAL, "ro.serialno")] = ""
|
|
properties[(USB_SERIAL, "ro.boot.serialno")] = ""
|
|
|
|
inspection = AdbClient(FakeRunner(output, properties)).inspect(USB_SERIAL)
|
|
|
|
self.assertEqual(inspection.model, "PKG110")
|
|
|
|
def test_multiple_online_devices_without_hardware_identity_are_unconfirmed(self) -> None:
|
|
output = (
|
|
"List of devices attached\n"
|
|
f"{WIFI_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
f"{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
)
|
|
properties = _properties((WIFI_SERIAL, USB_SERIAL))
|
|
properties[(WIFI_SERIAL, "ro.serialno")] = ""
|
|
properties[(WIFI_SERIAL, "ro.boot.serialno")] = ""
|
|
|
|
with self.assertRaises(DeviceIdentityUnconfirmedError):
|
|
AdbClient(FakeRunner(output, properties)).inspect(USB_SERIAL)
|
|
|
|
def test_multiple_online_devices_with_different_identity_keep_explicit_selection(self) -> None:
|
|
output = (
|
|
"List of devices attached\n"
|
|
f"{WIFI_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
f"{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
)
|
|
properties = _properties((WIFI_SERIAL, USB_SERIAL))
|
|
properties[(WIFI_SERIAL, "ro.serialno")] = "physical-phone-2"
|
|
properties[(WIFI_SERIAL, "ro.boot.serialno")] = "physical-phone-2"
|
|
|
|
inspection = AdbClient(FakeRunner(output, properties)).inspect(USB_SERIAL)
|
|
|
|
self.assertEqual(inspection.device.serial, USB_SERIAL)
|
|
|
|
def test_shared_boot_serial_is_duplicate_even_when_ro_serial_differs(self) -> None:
|
|
output = (
|
|
"List of devices attached\n"
|
|
f"{WIFI_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
f"{USB_SERIAL}\tdevice product:PKG110 model:PKG110 device:OP5D2BL1\n"
|
|
)
|
|
properties = _properties((WIFI_SERIAL, USB_SERIAL))
|
|
properties[(WIFI_SERIAL, "ro.serialno")] = "wifi-transport-serial"
|
|
properties[(USB_SERIAL, "ro.serialno")] = "usb-transport-serial"
|
|
properties[(WIFI_SERIAL, "ro.boot.serialno")] = "shared-hardware-serial"
|
|
properties[(USB_SERIAL, "ro.boot.serialno")] = "shared-hardware-serial"
|
|
|
|
with self.assertRaises(DuplicatePhysicalDeviceError):
|
|
AdbClient(FakeRunner(output, properties)).inspect(USB_SERIAL)
|
|
|
|
def test_unknown_adb_state_is_rejected(self) -> None:
|
|
client = AdbClient(FakeRunner(f"List of devices attached\n{USB_SERIAL}\trecovery\n", {}))
|
|
|
|
with self.assertRaises(DeviceStateError):
|
|
client.inspect(USB_SERIAL)
|
|
|
|
def test_runner_timeout_is_a_distinct_connection_error(self) -> None:
|
|
class TimeoutRunner:
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
raise subprocess.TimeoutExpired(arguments, timeout_seconds)
|
|
|
|
with self.assertRaises(DeviceCommandTimeoutError):
|
|
AdbClient(TimeoutRunner()).inspect(USB_SERIAL)
|
|
|
|
def test_product_intent_is_fixed_to_action_view_and_pdd_package(self) -> None:
|
|
class IntentRunner:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, ...]] = []
|
|
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
self.calls.append(tuple(arguments))
|
|
return CommandResult(stdout="Status: ok\n")
|
|
|
|
runner = IntentRunner()
|
|
summary = AdbClient(runner).start_pdd_view_intent(
|
|
USB_SERIAL,
|
|
"123",
|
|
)
|
|
|
|
self.assertEqual(summary.status, "ok")
|
|
self.assertEqual(
|
|
runner.calls,
|
|
[
|
|
(
|
|
"-s",
|
|
USB_SERIAL,
|
|
"shell",
|
|
"am",
|
|
"start",
|
|
"-W",
|
|
"-a",
|
|
"android.intent.action.VIEW",
|
|
"-d",
|
|
"https://mobile.yangkeduo.com/goods.html?goods_id=123",
|
|
"-p",
|
|
"com.xunmeng.pinduoduo",
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_product_intent_without_explicit_success_is_rejected(self) -> None:
|
|
class UnknownIntentRunner:
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
return CommandResult(stdout="Starting: Intent { ... }\n")
|
|
|
|
with self.assertRaises(IntentLaunchUnconfirmedError):
|
|
AdbClient(UnknownIntentRunner()).start_pdd_view_intent(
|
|
USB_SERIAL,
|
|
"123",
|
|
)
|
|
|
|
def test_product_intent_rejects_invalid_goods_id_before_runner(self) -> None:
|
|
class RecordingRunner:
|
|
def __init__(self) -> None:
|
|
self.calls: list[tuple[str, ...]] = []
|
|
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
self.calls.append(tuple(arguments))
|
|
return CommandResult(stdout="Status: ok\n")
|
|
|
|
invalid_values: tuple[object, ...] = (
|
|
"",
|
|
"12a",
|
|
"123",
|
|
" 123",
|
|
"123 ",
|
|
"https://mobile.yangkeduo.com/goods.html?goods_id=123",
|
|
"am start -W -d anything",
|
|
123,
|
|
None,
|
|
)
|
|
for value in invalid_values:
|
|
with self.subTest(value=repr(value)):
|
|
runner = RecordingRunner()
|
|
with self.assertRaises(ValueError):
|
|
AdbClient(runner).start_pdd_view_intent(USB_SERIAL, value) # type: ignore[arg-type]
|
|
self.assertEqual(runner.calls, [])
|
|
|
|
def test_product_intent_nonzero_and_timeout_remain_distinct(self) -> None:
|
|
class FailedIntentRunner:
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
return CommandResult(stdout="sensitive command output", returncode=1)
|
|
|
|
class TimeoutIntentRunner:
|
|
def run(self, arguments: Sequence[str], timeout_seconds: float) -> CommandResult:
|
|
raise subprocess.TimeoutExpired(arguments, timeout_seconds)
|
|
|
|
with self.assertRaises(DeviceCommandError) as command_error:
|
|
AdbClient(FailedIntentRunner()).start_pdd_view_intent(USB_SERIAL, "123")
|
|
self.assertNotIn("sensitive command output", str(command_error.exception))
|
|
|
|
with self.assertRaises(DeviceCommandTimeoutError):
|
|
AdbClient(TimeoutIntentRunner()).start_pdd_view_intent(USB_SERIAL, "123")
|