fix(client): read Android 16 foreground exactly
This commit is contained in:
@@ -14,6 +14,7 @@ sys.path.insert(0, str(CLIENT_ROOT / "src"))
|
|||||||
from cmbuyer_client.device.adb import AdbClient, DeviceConnectionError, SubprocessAdbRunner
|
from cmbuyer_client.device.adb import AdbClient, DeviceConnectionError, SubprocessAdbRunner
|
||||||
from cmbuyer_client.device.baseline import NoReconnectUiautomatorConnector
|
from cmbuyer_client.device.baseline import NoReconnectUiautomatorConnector
|
||||||
from cmbuyer_client.pdd.quantity_gate2_spike import (
|
from cmbuyer_client.pdd.quantity_gate2_spike import (
|
||||||
|
Android16TopResumedForegroundReader,
|
||||||
DECLARED_QUANTITIES,
|
DECLARED_QUANTITIES,
|
||||||
EXPECTED_GOODS_ID,
|
EXPECTED_GOODS_ID,
|
||||||
QuantityGate2EvidenceCapturer,
|
QuantityGate2EvidenceCapturer,
|
||||||
@@ -84,12 +85,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
print("失败:缺少 uiautomator2;请在采购工具虚拟环境中运行。", file=sys.stderr)
|
print("失败:缺少 uiautomator2;请在采购工具虚拟环境中运行。", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
adb_runner = SubprocessAdbRunner(arguments.adb)
|
||||||
capturer = QuantityGate2EvidenceCapturer(
|
capturer = QuantityGate2EvidenceCapturer(
|
||||||
AdbClient(SubprocessAdbRunner(arguments.adb), timeout_seconds=arguments.timeout),
|
AdbClient(adb_runner, timeout_seconds=arguments.timeout),
|
||||||
NoReconnectUiautomatorConnector(
|
NoReconnectUiautomatorConnector(
|
||||||
adbutils.AdbClient(socket_timeout=arguments.timeout).device_list,
|
adbutils.AdbClient(socket_timeout=arguments.timeout).device_list,
|
||||||
u2.connect,
|
u2.connect,
|
||||||
),
|
),
|
||||||
|
Android16TopResumedForegroundReader(adb_runner, arguments.timeout),
|
||||||
timeout_seconds=arguments.timeout,
|
timeout_seconds=arguments.timeout,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
from math import isfinite
|
from math import isfinite
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Any, Protocol
|
from typing import Any, Protocol
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -18,7 +19,7 @@ from adbutils.errors import AdbTimeout
|
|||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
from uiautomator2.exceptions import HTTPTimeoutError
|
from uiautomator2.exceptions import HTTPTimeoutError
|
||||||
|
|
||||||
from ..device.adb import AdbClient, DeviceConnectionError, DeviceInspection
|
from ..device.adb import AdbClient, CommandRunner, DeviceConnectionError, DeviceInspection
|
||||||
from ..device.baseline import (
|
from ..device.baseline import (
|
||||||
HIERARCHY_PARAMS,
|
HIERARCHY_PARAMS,
|
||||||
PDD_PACKAGE,
|
PDD_PACKAGE,
|
||||||
@@ -52,13 +53,50 @@ class QuantityGate2ReadDevice(Protocol):
|
|||||||
|
|
||||||
def app_info(self, package_name: str) -> dict[str, Any]: ...
|
def app_info(self, package_name: str) -> dict[str, Any]: ...
|
||||||
|
|
||||||
def app_current(self) -> dict[str, Any]: ...
|
|
||||||
|
|
||||||
def window_size(self) -> tuple[int, int]: ...
|
def window_size(self) -> tuple[int, int]: ...
|
||||||
|
|
||||||
def jsonrpc_call(self, method: str, params: Any = None, timeout: float = 10) -> Any: ...
|
def jsonrpc_call(self, method: str, params: Any = None, timeout: float = 10) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
class QuantityGate2ForegroundReader(Protocol):
|
||||||
|
"""只读取 Android 16 的唯一 resumed activity,不暴露通用 shell。"""
|
||||||
|
|
||||||
|
def read(self, serial: str) -> dict[str, str]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Android16TopResumedForegroundReader:
|
||||||
|
"""绕开 adbutils 2.12.0 对 Android 16 ``topResumedActivity`` 的误解析。"""
|
||||||
|
|
||||||
|
_TOP_RESUMED_PATTERN = re.compile(
|
||||||
|
r"(?m)^\s*topResumedActivity=ActivityRecord\{[^\r\n}]*?\s+u\d+\s+"
|
||||||
|
r"(?P<package>[^/\s]+)/(?P<activity>[^\s}]+)\s+t\d+\}\s*$"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, runner: CommandRunner, timeout_seconds: float) -> None:
|
||||||
|
if not _is_positive_finite(timeout_seconds):
|
||||||
|
raise ValueError("timeout_seconds 必须是大于 0 的有限数值")
|
||||||
|
self._runner = runner
|
||||||
|
self._timeout_seconds = timeout_seconds
|
||||||
|
|
||||||
|
def read(self, serial: str) -> dict[str, str]:
|
||||||
|
if type(serial) is not str or not serial.strip() or serial != serial.strip():
|
||||||
|
raise QuantityGate2EvidenceError("必须显式提供非空设备通道。")
|
||||||
|
result = self._runner.run(
|
||||||
|
("-s", serial, "shell", "dumpsys", "activity", "activities"),
|
||||||
|
self._timeout_seconds,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise QuantityGate2EvidenceError("Android 前台摘要读取失败,未发布证据。")
|
||||||
|
matches = list(self._TOP_RESUMED_PATTERN.finditer(result.stdout))
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise QuantityGate2EvidenceError("Android 前台摘要不唯一,未发布证据。")
|
||||||
|
match = matches[0]
|
||||||
|
return {
|
||||||
|
"package": match.group("package"),
|
||||||
|
"activity": match.group("activity"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class QuantityGate2EvidenceResult:
|
class QuantityGate2EvidenceResult:
|
||||||
output_directory: Path
|
output_directory: Path
|
||||||
@@ -75,12 +113,14 @@ class QuantityGate2EvidenceCapturer:
|
|||||||
self,
|
self,
|
||||||
adb_client: AdbClient,
|
adb_client: AdbClient,
|
||||||
connector: Callable[[str], QuantityGate2ReadDevice],
|
connector: Callable[[str], QuantityGate2ReadDevice],
|
||||||
|
foreground_reader: QuantityGate2ForegroundReader,
|
||||||
timeout_seconds: float,
|
timeout_seconds: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not _is_positive_finite(timeout_seconds):
|
if not _is_positive_finite(timeout_seconds):
|
||||||
raise ValueError("timeout_seconds 必须是大于 0 的有限数值")
|
raise ValueError("timeout_seconds 必须是大于 0 的有限数值")
|
||||||
self._adb_client = adb_client
|
self._adb_client = adb_client
|
||||||
self._connector = connector
|
self._connector = connector
|
||||||
|
self._foreground_reader = foreground_reader
|
||||||
self._timeout_seconds = timeout_seconds
|
self._timeout_seconds = timeout_seconds
|
||||||
self._started = False
|
self._started = False
|
||||||
|
|
||||||
@@ -104,7 +144,10 @@ class QuantityGate2EvidenceCapturer:
|
|||||||
inspection = self._adb_client.inspect(serial)
|
inspection = self._adb_client.inspect(serial)
|
||||||
_require_expected_device(inspection)
|
_require_expected_device(inspection)
|
||||||
device = self._connector(serial)
|
device = self._connector(serial)
|
||||||
initial_app = _require_read_precondition(device)
|
initial_app = _require_read_precondition(
|
||||||
|
device,
|
||||||
|
self._foreground_reader.read(serial),
|
||||||
|
)
|
||||||
|
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
staging = target.parent / f".{target.name}.staging-{uuid4().hex}"
|
staging = target.parent / f".{target.name}.staging-{uuid4().hex}"
|
||||||
@@ -132,7 +175,10 @@ class QuantityGate2EvidenceCapturer:
|
|||||||
hierarchy_path = staging / "hierarchy.xml"
|
hierarchy_path = staging / "hierarchy.xml"
|
||||||
hierarchy_path.write_text(hierarchy, encoding="utf-8")
|
hierarchy_path.write_text(hierarchy, encoding="utf-8")
|
||||||
|
|
||||||
final_app = _require_read_precondition(device)
|
final_app = _require_read_precondition(
|
||||||
|
device,
|
||||||
|
self._foreground_reader.read(serial),
|
||||||
|
)
|
||||||
if final_app != initial_app:
|
if final_app != initial_app:
|
||||||
raise QuantityGate2EvidenceError("数量状态取证期间前台页面漂移,未发布证据。")
|
raise QuantityGate2EvidenceError("数量状态取证期间前台页面漂移,未发布证据。")
|
||||||
app_path = staging / "app.json"
|
app_path = staging / "app.json"
|
||||||
@@ -216,12 +262,14 @@ def _require_expected_device(inspection: DeviceInspection) -> None:
|
|||||||
raise QuantityGate2EvidenceError("设备不是已批准取证组合。")
|
raise QuantityGate2EvidenceError("设备不是已批准取证组合。")
|
||||||
|
|
||||||
|
|
||||||
def _require_read_precondition(device: QuantityGate2ReadDevice) -> dict[str, str]:
|
def _require_read_precondition(
|
||||||
|
device: QuantityGate2ReadDevice,
|
||||||
|
current: object,
|
||||||
|
) -> dict[str, str]:
|
||||||
info = device.app_info(PDD_PACKAGE)
|
info = device.app_info(PDD_PACKAGE)
|
||||||
version = (info.get("versionName") or info.get("version_name")) if isinstance(info, dict) else None
|
version = (info.get("versionName") or info.get("version_name")) if isinstance(info, dict) else None
|
||||||
if version != EXPECTED_PDD_VERSION:
|
if version != EXPECTED_PDD_VERSION:
|
||||||
raise QuantityGate2EvidenceError("拼多多版本不是已批准取证版本。")
|
raise QuantityGate2EvidenceError("拼多多版本不是已批准取证版本。")
|
||||||
current = device.app_current()
|
|
||||||
if not isinstance(current, dict) or current.get("package") != PDD_PACKAGE:
|
if not isinstance(current, dict) or current.get("package") != PDD_PACKAGE:
|
||||||
raise QuantityGate2EvidenceError("拼多多不在前台。")
|
raise QuantityGate2EvidenceError("拼多多不在前台。")
|
||||||
activity = current.get("activity")
|
activity = current.get("activity")
|
||||||
|
|||||||
@@ -22,19 +22,25 @@ from PIL import Image
|
|||||||
CLIENT_ROOT = Path(__file__).resolve().parents[2]
|
CLIENT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(CLIENT_ROOT / "src"))
|
sys.path.insert(0, str(CLIENT_ROOT / "src"))
|
||||||
|
|
||||||
from cmbuyer_client.device.adb import AdbDevice, DeviceInspection
|
from cmbuyer_client.device.adb import AdbDevice, CommandResult, DeviceInspection
|
||||||
from cmbuyer_client.pdd.quantity_gate2_spike import (
|
from cmbuyer_client.pdd.quantity_gate2_spike import (
|
||||||
|
Android16TopResumedForegroundReader,
|
||||||
DECLARED_QUANTITIES,
|
DECLARED_QUANTITIES,
|
||||||
EXPECTED_GOODS_ID,
|
EXPECTED_GOODS_ID,
|
||||||
QuantityGate2EvidenceCapturer,
|
QuantityGate2EvidenceCapturer,
|
||||||
QuantityGate2EvidenceError,
|
QuantityGate2EvidenceError,
|
||||||
QuantityGate2EvidenceTimeoutError,
|
QuantityGate2EvidenceTimeoutError,
|
||||||
|
QuantityGate2ForegroundReader,
|
||||||
QuantityGate2ReadDevice,
|
QuantityGate2ReadDevice,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SERIAL = "192.168.0.173:5555"
|
SERIAL = "192.168.0.173:5555"
|
||||||
HIERARCHY = "<?xml version='1.0' encoding='UTF-8'?><hierarchy rotation='0'><node text='local raw page' /></hierarchy>"
|
HIERARCHY = "<?xml version='1.0' encoding='UTF-8'?><hierarchy rotation='0'><node text='local raw page' /></hierarchy>"
|
||||||
|
TOP_RESUMED = (
|
||||||
|
" topResumedActivity=ActivityRecord{101034589 u0 "
|
||||||
|
"com.xunmeng.pinduoduo/.activity.NewPageActivity t1816}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
@@ -63,25 +69,18 @@ class FakeReadDevice:
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
version: str = "8.17.0",
|
version: str = "8.17.0",
|
||||||
package: str = "com.xunmeng.pinduoduo",
|
|
||||||
activity: str = "com.xunmeng.pinduoduo.activity.NewPageActivity",
|
|
||||||
screen_size: tuple[int, int] = (1080, 2376),
|
screen_size: tuple[int, int] = (1080, 2376),
|
||||||
screenshot: object | None = None,
|
screenshot: object | None = None,
|
||||||
hierarchy: object = HIERARCHY,
|
hierarchy: object = HIERARCHY,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.version = version
|
self.version = version
|
||||||
self.package = package
|
|
||||||
self.activity = activity
|
|
||||||
self.screen_size = screen_size
|
self.screen_size = screen_size
|
||||||
self.screenshot = _png_base64() if screenshot is None else screenshot
|
self.screenshot = _png_base64() if screenshot is None else screenshot
|
||||||
self.hierarchy = hierarchy
|
self.hierarchy = hierarchy
|
||||||
self.calls: list[tuple[object, ...]] = []
|
self.calls: list[tuple[object, ...]] = []
|
||||||
self.app_info_reads = 0
|
self.app_info_reads = 0
|
||||||
self.current_reads = 0
|
|
||||||
self.window_reads = 0
|
self.window_reads = 0
|
||||||
self.post_version: str | None = None
|
self.post_version: str | None = None
|
||||||
self.post_package: str | None = None
|
|
||||||
self.post_activity: str | None = None
|
|
||||||
self.post_screen_size: tuple[int, int] | None = None
|
self.post_screen_size: tuple[int, int] | None = None
|
||||||
|
|
||||||
def app_info(self, package_name: str) -> dict[str, str]:
|
def app_info(self, package_name: str) -> dict[str, str]:
|
||||||
@@ -90,13 +89,6 @@ class FakeReadDevice:
|
|||||||
version = self.post_version if self.app_info_reads > 1 and self.post_version is not None else self.version
|
version = self.post_version if self.app_info_reads > 1 and self.post_version is not None else self.version
|
||||||
return {"versionName": version}
|
return {"versionName": version}
|
||||||
|
|
||||||
def app_current(self) -> dict[str, str]:
|
|
||||||
self.calls.append(("app_current",))
|
|
||||||
self.current_reads += 1
|
|
||||||
package = self.post_package if self.current_reads > 1 and self.post_package is not None else self.package
|
|
||||||
activity = self.post_activity if self.current_reads > 1 and self.post_activity is not None else self.activity
|
|
||||||
return {"package": package, "activity": activity}
|
|
||||||
|
|
||||||
def window_size(self) -> tuple[int, int]:
|
def window_size(self) -> tuple[int, int]:
|
||||||
self.calls.append(("window_size",))
|
self.calls.append(("window_size",))
|
||||||
self.window_reads += 1
|
self.window_reads += 1
|
||||||
@@ -113,6 +105,36 @@ class FakeReadDevice:
|
|||||||
raise AssertionError(f"unexpected RPC {method}")
|
raise AssertionError(f"unexpected RPC {method}")
|
||||||
|
|
||||||
|
|
||||||
|
class FakeForegroundReader:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
package: str = "com.xunmeng.pinduoduo",
|
||||||
|
activity: str = ".activity.NewPageActivity",
|
||||||
|
) -> None:
|
||||||
|
self.package = package
|
||||||
|
self.activity = activity
|
||||||
|
self.post_package: str | None = None
|
||||||
|
self.post_activity: str | None = None
|
||||||
|
self.calls: list[str] = []
|
||||||
|
|
||||||
|
def read(self, serial: str) -> dict[str, str]:
|
||||||
|
self.calls.append(serial)
|
||||||
|
package = self.post_package if len(self.calls) > 1 and self.post_package is not None else self.package
|
||||||
|
activity = self.post_activity if len(self.calls) > 1 and self.post_activity is not None else self.activity
|
||||||
|
return {"package": package, "activity": activity}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCommandRunner:
|
||||||
|
def __init__(self, result: CommandResult) -> None:
|
||||||
|
self.result = result
|
||||||
|
self.calls: list[tuple[tuple[str, ...], float]] = []
|
||||||
|
|
||||||
|
def run(self, arguments: tuple[str, ...], timeout_seconds: float) -> CommandResult:
|
||||||
|
self.calls.append((arguments, timeout_seconds))
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
def _load_script() -> object:
|
def _load_script() -> object:
|
||||||
script_path = CLIENT_ROOT / "scripts" / "capture_quantity_gate2_spike.py"
|
script_path = CLIENT_ROOT / "scripts" / "capture_quantity_gate2_spike.py"
|
||||||
spec = spec_from_file_location("capture_quantity_gate2_spike_for_test", script_path)
|
spec = spec_from_file_location("capture_quantity_gate2_spike_for_test", script_path)
|
||||||
@@ -136,12 +158,71 @@ def _namespace(**changes: object) -> argparse.Namespace:
|
|||||||
return argparse.Namespace(**values)
|
return argparse.Namespace(**values)
|
||||||
|
|
||||||
|
|
||||||
|
class Android16TopResumedForegroundReaderTests(unittest.TestCase):
|
||||||
|
def test_reads_exact_unique_android16_top_resumed_activity(self) -> None:
|
||||||
|
runner = FakeCommandRunner(CommandResult(stdout=TOP_RESUMED))
|
||||||
|
reader = Android16TopResumedForegroundReader(runner, timeout_seconds=7)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
reader.read(SERIAL),
|
||||||
|
{
|
||||||
|
"package": "com.xunmeng.pinduoduo",
|
||||||
|
"activity": ".activity.NewPageActivity",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
runner.calls,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"-s",
|
||||||
|
SERIAL,
|
||||||
|
"shell",
|
||||||
|
"dumpsys",
|
||||||
|
"activity",
|
||||||
|
"activities",
|
||||||
|
),
|
||||||
|
7,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_missing_legacy_or_duplicate_top_resumed_activity_is_rejected(self) -> None:
|
||||||
|
rejected_outputs = (
|
||||||
|
"",
|
||||||
|
(
|
||||||
|
"mResumedActivity: ActivityRecord{101034589 u0 "
|
||||||
|
"com.xunmeng.pinduoduo/.activity.NewPageActivity t1816}\n"
|
||||||
|
),
|
||||||
|
TOP_RESUMED + TOP_RESUMED,
|
||||||
|
"topResumedActivity=null\n",
|
||||||
|
)
|
||||||
|
for output in rejected_outputs:
|
||||||
|
with self.subTest(output=output):
|
||||||
|
reader = Android16TopResumedForegroundReader(
|
||||||
|
FakeCommandRunner(CommandResult(stdout=output)),
|
||||||
|
timeout_seconds=7,
|
||||||
|
)
|
||||||
|
with self.assertRaises(QuantityGate2EvidenceError):
|
||||||
|
reader.read(SERIAL)
|
||||||
|
|
||||||
|
def test_command_failure_and_invalid_serial_are_rejected(self) -> None:
|
||||||
|
runner = FakeCommandRunner(CommandResult(stdout=TOP_RESUMED, returncode=1))
|
||||||
|
reader = Android16TopResumedForegroundReader(runner, timeout_seconds=7)
|
||||||
|
with self.assertRaises(QuantityGate2EvidenceError):
|
||||||
|
reader.read(SERIAL)
|
||||||
|
with self.assertRaises(QuantityGate2EvidenceError):
|
||||||
|
reader.read(f" {SERIAL}")
|
||||||
|
self.assertEqual(len(runner.calls), 1)
|
||||||
|
|
||||||
|
|
||||||
class QuantityGate2EvidenceTests(unittest.TestCase):
|
class QuantityGate2EvidenceTests(unittest.TestCase):
|
||||||
def _capturer(
|
def _capturer(
|
||||||
self,
|
self,
|
||||||
adb: FakeAdbClient,
|
adb: FakeAdbClient,
|
||||||
device: FakeReadDevice,
|
device: FakeReadDevice,
|
||||||
*,
|
*,
|
||||||
|
foreground: FakeForegroundReader | None = None,
|
||||||
connector_calls: list[str] | None = None,
|
connector_calls: list[str] | None = None,
|
||||||
) -> QuantityGate2EvidenceCapturer:
|
) -> QuantityGate2EvidenceCapturer:
|
||||||
def connect(serial: str) -> FakeReadDevice:
|
def connect(serial: str) -> FakeReadDevice:
|
||||||
@@ -149,17 +230,28 @@ class QuantityGate2EvidenceTests(unittest.TestCase):
|
|||||||
connector_calls.append(serial)
|
connector_calls.append(serial)
|
||||||
return device
|
return device
|
||||||
|
|
||||||
return QuantityGate2EvidenceCapturer(adb, connect, timeout_seconds=2)
|
return QuantityGate2EvidenceCapturer(
|
||||||
|
adb,
|
||||||
|
connect,
|
||||||
|
foreground or FakeForegroundReader(),
|
||||||
|
timeout_seconds=2,
|
||||||
|
)
|
||||||
|
|
||||||
def test_initial_and_target_states_publish_only_raw_read_evidence(self) -> None:
|
def test_initial_and_target_states_publish_only_raw_read_evidence(self) -> None:
|
||||||
for state, quantity in DECLARED_QUANTITIES.items():
|
for state, quantity in DECLARED_QUANTITIES.items():
|
||||||
with self.subTest(state=state), TemporaryDirectory() as temporary:
|
with self.subTest(state=state), TemporaryDirectory() as temporary:
|
||||||
adb = FakeAdbClient()
|
adb = FakeAdbClient()
|
||||||
device = FakeReadDevice()
|
device = FakeReadDevice()
|
||||||
|
foreground = FakeForegroundReader()
|
||||||
connector_calls: list[str] = []
|
connector_calls: list[str] = []
|
||||||
target = Path(temporary) / state
|
target = Path(temporary) / state
|
||||||
|
|
||||||
result = self._capturer(adb, device, connector_calls=connector_calls).capture(
|
result = self._capturer(
|
||||||
|
adb,
|
||||||
|
device,
|
||||||
|
foreground=foreground,
|
||||||
|
connector_calls=connector_calls,
|
||||||
|
).capture(
|
||||||
SERIAL,
|
SERIAL,
|
||||||
EXPECTED_GOODS_ID,
|
EXPECTED_GOODS_ID,
|
||||||
state,
|
state,
|
||||||
@@ -171,6 +263,7 @@ class QuantityGate2EvidenceTests(unittest.TestCase):
|
|||||||
app = json.loads(result.app_path.read_text(encoding="utf-8"))
|
app = json.loads(result.app_path.read_text(encoding="utf-8"))
|
||||||
self.assertEqual(adb.calls, [SERIAL])
|
self.assertEqual(adb.calls, [SERIAL])
|
||||||
self.assertEqual(connector_calls, [SERIAL])
|
self.assertEqual(connector_calls, [SERIAL])
|
||||||
|
self.assertEqual(foreground.calls, [SERIAL, SERIAL])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[call[1] for call in device.calls if call[0] == "jsonrpc"],
|
[call[1] for call in device.calls if call[0] == "jsonrpc"],
|
||||||
["takeScreenshot", "dumpWindowHierarchy"],
|
["takeScreenshot", "dumpWindowHierarchy"],
|
||||||
@@ -236,18 +329,18 @@ class QuantityGate2EvidenceTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_device_version_package_activity_and_screen_mismatch_fail_closed(self) -> None:
|
def test_device_version_package_activity_and_screen_mismatch_fail_closed(self) -> None:
|
||||||
scenarios = (
|
scenarios = (
|
||||||
(FakeAdbClient(model="OTHER"), FakeReadDevice()),
|
(FakeAdbClient(model="OTHER"), FakeReadDevice(), FakeForegroundReader()),
|
||||||
(FakeAdbClient(android_version="15"), FakeReadDevice()),
|
(FakeAdbClient(android_version="15"), FakeReadDevice(), FakeForegroundReader()),
|
||||||
(FakeAdbClient(), FakeReadDevice(version="8.17.1")),
|
(FakeAdbClient(), FakeReadDevice(version="8.17.1"), FakeForegroundReader()),
|
||||||
(FakeAdbClient(), FakeReadDevice(package="com.example.other")),
|
(FakeAdbClient(), FakeReadDevice(), FakeForegroundReader(package="com.example.other")),
|
||||||
(FakeAdbClient(), FakeReadDevice(activity="")),
|
(FakeAdbClient(), FakeReadDevice(), FakeForegroundReader(activity="")),
|
||||||
(FakeAdbClient(), FakeReadDevice(screen_size=(1080, 2400))),
|
(FakeAdbClient(), FakeReadDevice(screen_size=(1080, 2400)), FakeForegroundReader()),
|
||||||
)
|
)
|
||||||
for adb, device in scenarios:
|
for adb, device, foreground in scenarios:
|
||||||
with self.subTest(device=device.__dict__), TemporaryDirectory() as temporary:
|
with self.subTest(device=device.__dict__), TemporaryDirectory() as temporary:
|
||||||
target = Path(temporary) / "evidence"
|
target = Path(temporary) / "evidence"
|
||||||
with self.assertRaises(QuantityGate2EvidenceError):
|
with self.assertRaises(QuantityGate2EvidenceError):
|
||||||
self._capturer(adb, device).capture(
|
self._capturer(adb, device, foreground=foreground).capture(
|
||||||
SERIAL, EXPECTED_GOODS_ID, "initial", 1, target
|
SERIAL, EXPECTED_GOODS_ID, "initial", 1, target
|
||||||
)
|
)
|
||||||
self.assertFalse(target.exists())
|
self.assertFalse(target.exists())
|
||||||
@@ -295,18 +388,23 @@ class QuantityGate2EvidenceTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_post_capture_app_or_screen_drift_does_not_publish(self) -> None:
|
def test_post_capture_app_or_screen_drift_does_not_publish(self) -> None:
|
||||||
drift_mutators = (
|
drift_mutators = (
|
||||||
lambda device: setattr(device, "post_version", "8.17.1"),
|
lambda device, foreground: setattr(device, "post_version", "8.17.1"),
|
||||||
lambda device: setattr(device, "post_package", "com.example.other"),
|
lambda device, foreground: setattr(foreground, "post_package", "com.example.other"),
|
||||||
lambda device: setattr(device, "post_activity", "OtherActivity"),
|
lambda device, foreground: setattr(foreground, "post_activity", "OtherActivity"),
|
||||||
lambda device: setattr(device, "post_screen_size", (1080, 2400)),
|
lambda device, foreground: setattr(device, "post_screen_size", (1080, 2400)),
|
||||||
)
|
)
|
||||||
for mutate in drift_mutators:
|
for mutate in drift_mutators:
|
||||||
with TemporaryDirectory() as temporary:
|
with TemporaryDirectory() as temporary:
|
||||||
device = FakeReadDevice()
|
device = FakeReadDevice()
|
||||||
mutate(device)
|
foreground = FakeForegroundReader()
|
||||||
|
mutate(device, foreground)
|
||||||
target = Path(temporary) / "evidence"
|
target = Path(temporary) / "evidence"
|
||||||
with self.assertRaises(QuantityGate2EvidenceError):
|
with self.assertRaises(QuantityGate2EvidenceError):
|
||||||
self._capturer(FakeAdbClient(), device).capture(
|
self._capturer(
|
||||||
|
FakeAdbClient(),
|
||||||
|
device,
|
||||||
|
foreground=foreground,
|
||||||
|
).capture(
|
||||||
SERIAL, EXPECTED_GOODS_ID, "initial", 1, target
|
SERIAL, EXPECTED_GOODS_ID, "initial", 1, target
|
||||||
)
|
)
|
||||||
self.assertFalse(target.exists())
|
self.assertFalse(target.exists())
|
||||||
@@ -336,6 +434,15 @@ class QuantityGate2EvidenceTests(unittest.TestCase):
|
|||||||
"pay",
|
"pay",
|
||||||
}
|
}
|
||||||
self.assertTrue(forbidden.isdisjoint(QuantityGate2ReadDevice.__dict__))
|
self.assertTrue(forbidden.isdisjoint(QuantityGate2ReadDevice.__dict__))
|
||||||
|
self.assertTrue(forbidden.isdisjoint(QuantityGate2ForegroundReader.__dict__))
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
name
|
||||||
|
for name in Android16TopResumedForegroundReader.__dict__
|
||||||
|
if not name.startswith("_")
|
||||||
|
},
|
||||||
|
{"read"},
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{name for name in QuantityGate2EvidenceCapturer.__dict__ if not name.startswith("_")},
|
{name for name in QuantityGate2EvidenceCapturer.__dict__ if not name.startswith("_")},
|
||||||
{"capture"},
|
{"capture"},
|
||||||
|
|||||||
Reference in New Issue
Block a user