devhelmkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devhelmkit/__init__.py +44 -0
- devhelmkit/android/__init__.py +7 -0
- devhelmkit/android/driver.py +18 -0
- devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
- devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
- devhelmkit/assets/so/x86_64/agent.so +0 -0
- devhelmkit/core/__init__.py +3 -0
- devhelmkit/core/base_component.py +214 -0
- devhelmkit/core/base_driver.py +414 -0
- devhelmkit/core/base_window.py +25 -0
- devhelmkit/core/selector_spec.py +102 -0
- devhelmkit/entry.py +90 -0
- devhelmkit/exceptions.py +56 -0
- devhelmkit/harmony/__init__.py +3 -0
- devhelmkit/harmony/agent/__init__.py +7 -0
- devhelmkit/harmony/agent/so_manager.py +300 -0
- devhelmkit/harmony/config.py +124 -0
- devhelmkit/harmony/device/__init__.py +6 -0
- devhelmkit/harmony/device/hdc.py +430 -0
- devhelmkit/harmony/driver.py +1416 -0
- devhelmkit/harmony/finder/__init__.py +12 -0
- devhelmkit/harmony/finder/component_finder.py +336 -0
- devhelmkit/harmony/finder/popup_handler.py +81 -0
- devhelmkit/harmony/finder/selector_adapter.py +101 -0
- devhelmkit/harmony/finder/xpath_query.py +110 -0
- devhelmkit/harmony/rpc/__init__.py +12 -0
- devhelmkit/harmony/rpc/client.py +126 -0
- devhelmkit/harmony/rpc/proxy_v2.py +106 -0
- devhelmkit/harmony/rpc/remote_object.py +48 -0
- devhelmkit/harmony/uiobject.py +246 -0
- devhelmkit/harmony/uiwindow.py +43 -0
- devhelmkit/harmony/webview/__init__.py +17 -0
- devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
- devhelmkit/harmony/webview/devtools_finder.py +131 -0
- devhelmkit/harmony/webview/webview_driver.py +326 -0
- devhelmkit/model/__init__.py +3 -0
- devhelmkit/model/action.py +147 -0
- devhelmkit/model/app_state.py +50 -0
- devhelmkit/model/constants.py +22 -0
- devhelmkit/model/display.py +32 -0
- devhelmkit/model/format_string.py +24 -0
- devhelmkit/model/input.py +50 -0
- devhelmkit/model/json_base.py +42 -0
- devhelmkit/model/keys.py +375 -0
- devhelmkit/model/match_pattern.py +15 -0
- devhelmkit/model/page.py +13 -0
- devhelmkit/model/params.py +42 -0
- devhelmkit/model/rect.py +58 -0
- devhelmkit/model/runnable.py +18 -0
- devhelmkit/utils/__init__.py +3 -0
- devhelmkit/utils/logger.py +72 -0
- devhelmkit/utils/retry.py +46 -0
- devhelmkit/utils/timeout.py +64 -0
- devhelmkit-0.1.0.dist-info/METADATA +411 -0
- devhelmkit-0.1.0.dist-info/RECORD +58 -0
- devhelmkit-0.1.0.dist-info/WHEEL +5 -0
- devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- devhelmkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""设备端 uitest 守护进程(agent.so)管理。
|
|
4
|
+
|
|
5
|
+
职责:
|
|
6
|
+
- 探测设备 ABI 与 uitest 协议版本(v1=5.0 / v2=6.0+)
|
|
7
|
+
- 按版本选择本地 agent.so(assets/so/{abi}/)
|
|
8
|
+
- MD5 校验避免重复推送
|
|
9
|
+
- 启动 uitest 守护进程并渐进式等待就绪
|
|
10
|
+
- 守护进程加载后清理设备端临时 so,避免残留与版本混淆
|
|
11
|
+
"""
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import shlex
|
|
16
|
+
import time
|
|
17
|
+
from typing import TYPE_CHECKING, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from devhelmkit.exceptions import AgentError, DeviceConnectError
|
|
20
|
+
from devhelmkit.utils import logger
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
24
|
+
|
|
25
|
+
# 设备端 agent.so 路径
|
|
26
|
+
DEVICE_AGENT_PATH = "/data/local/tmp/agent.so"
|
|
27
|
+
|
|
28
|
+
# 守护进程启动命令
|
|
29
|
+
DAEMON_COMMAND = "uitest start-daemon singleness"
|
|
30
|
+
|
|
31
|
+
# 协议版本阈值:uitest 版本 > 该值则为 v2(6.0+),否则 v1(5.0)
|
|
32
|
+
PROTOCOL_V2_THRESHOLD = "6.0.2.1"
|
|
33
|
+
|
|
34
|
+
# 守护进程启动渐进式等待(秒),总计约 410ms
|
|
35
|
+
DAEMON_START_DELAYS = (0.030, 0.050, 0.080, 0.100, 0.150)
|
|
36
|
+
|
|
37
|
+
# 本地 so 资产根目录(相对包根)
|
|
38
|
+
SO_ASSET_DIR = "assets/so"
|
|
39
|
+
|
|
40
|
+
# ABI → 协议版本 → so 文件名
|
|
41
|
+
# arm64-v8a 区分 v1/v2,x86_64 不区分(仅模拟器场景)
|
|
42
|
+
_AGENT_FILE_MAP = {
|
|
43
|
+
"arm64-v8a": {1: "agent_v1.so", 2: "agent_v2.so"},
|
|
44
|
+
"x86_64": {1: "agent.so", 2: "agent.so"},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _compare_version(a: str, b: str) -> int:
|
|
49
|
+
"""语义版本比较,返回 -1/0/1。缺失位按 0 处理。"""
|
|
50
|
+
pa = [int(x) for x in re.findall(r"\d+", a)]
|
|
51
|
+
pb = [int(x) for x in re.findall(r"\d+", b)]
|
|
52
|
+
for x, y in zip(pa, pb):
|
|
53
|
+
if x < y:
|
|
54
|
+
return -1
|
|
55
|
+
if x > y:
|
|
56
|
+
return 1
|
|
57
|
+
if len(pa) < len(pb):
|
|
58
|
+
return -1
|
|
59
|
+
if len(pa) > len(pb):
|
|
60
|
+
return 1
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_package_root() -> str:
|
|
65
|
+
"""获取 devhelmkit 包根目录绝对路径。
|
|
66
|
+
|
|
67
|
+
so_manager.py 位于 harmony/agent/so_manager.py,包根为上两级。
|
|
68
|
+
"""
|
|
69
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
70
|
+
return os.path.dirname(os.path.dirname(here))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AgentManager:
|
|
74
|
+
"""设备端 uitest 守护进程管理器。
|
|
75
|
+
|
|
76
|
+
管理流程:探测设备信息 → 检查守护进程 → 推送 agent.so → 启动守护进程 → 删除设备端 so。
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, device: 'HdcDevice'):
|
|
80
|
+
self._device = device
|
|
81
|
+
self._abi: Optional[str] = None
|
|
82
|
+
self._protocol_version: Optional[int] = None
|
|
83
|
+
|
|
84
|
+
def detect_device_info(self) -> Tuple[str, int]:
|
|
85
|
+
"""探测设备 ABI 与 uitest 协议版本。
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
(abi, protocol_version) 元组,protocol_version 为 1(5.0)或 2(6.0+)。
|
|
89
|
+
"""
|
|
90
|
+
abi = self._detect_abi()
|
|
91
|
+
protocol_version = self._detect_protocol_version()
|
|
92
|
+
self._abi = abi
|
|
93
|
+
self._protocol_version = protocol_version
|
|
94
|
+
logger.info("设备信息 (serial=%s): abi=%s, protocol=v%d",
|
|
95
|
+
self._device.serial, abi, protocol_version)
|
|
96
|
+
return abi, protocol_version
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def abi(self) -> Optional[str]:
|
|
100
|
+
return self._abi
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def protocol_version(self) -> Optional[int]:
|
|
104
|
+
return self._protocol_version
|
|
105
|
+
|
|
106
|
+
def ensure_daemon_running(self) -> None:
|
|
107
|
+
"""确保 uitest 守护进程已启动。
|
|
108
|
+
|
|
109
|
+
优先复用已有进程;未运行则推送 agent.so 并启动。
|
|
110
|
+
无论守护进程是否已运行,都需探测协议版本以正确建立端口转发。
|
|
111
|
+
"""
|
|
112
|
+
if self._abi is None or self._protocol_version is None:
|
|
113
|
+
self.detect_device_info()
|
|
114
|
+
|
|
115
|
+
if self._is_daemon_running():
|
|
116
|
+
logger.debug("uitest 守护进程已运行,复用 (serial=%s)",
|
|
117
|
+
self._device.serial)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
logger.info("uitest 守护进程未运行,准备启动 (serial=%s)",
|
|
121
|
+
self._device.serial)
|
|
122
|
+
self._deploy_agent_so(self._abi, self._protocol_version)
|
|
123
|
+
self._start_daemon_and_wait()
|
|
124
|
+
|
|
125
|
+
def stop_daemon(self) -> None:
|
|
126
|
+
"""停止设备端 uitest 守护进程。
|
|
127
|
+
|
|
128
|
+
复用 _is_daemon_running 的过滤逻辑定位目标 pid,避免误杀
|
|
129
|
+
scrcpy_server、extension-name 等扩展进程(其 cmdline 可能
|
|
130
|
+
含 uitest 关键字)。pkill -f 无法通过管道过滤目标,故改用
|
|
131
|
+
pgrep -fl 获取 pid+cmdline,过滤扩展进程后逐个 kill。
|
|
132
|
+
"""
|
|
133
|
+
if not self._is_daemon_running():
|
|
134
|
+
logger.debug("uitest 守护进程未运行,无需停止 (serial=%s)",
|
|
135
|
+
self._device.serial)
|
|
136
|
+
return
|
|
137
|
+
# pgrep -fl 输出 "pid cmdline",grep -v 过滤扩展进程与 pgrep 自身
|
|
138
|
+
cmd = ('pgrep -fl "uitest.*start-daemon.*singleness" '
|
|
139
|
+
'| grep -v "extension-name" | grep -v "scrcpy_server" '
|
|
140
|
+
'| grep -v "pgrep"')
|
|
141
|
+
try:
|
|
142
|
+
out = self._device.shell(cmd, timeout=10)
|
|
143
|
+
except DeviceConnectError as e:
|
|
144
|
+
logger.warn("获取 uitest 守护进程 pid 失败 (serial=%s): %s",
|
|
145
|
+
self._device.serial, e)
|
|
146
|
+
return
|
|
147
|
+
# pgrep -fl 输出格式 "pid cmdline",取首列 pid
|
|
148
|
+
pids = [line.split()[0] for line in out.splitlines() if line.strip()]
|
|
149
|
+
if not pids:
|
|
150
|
+
logger.debug("无匹配 pid,无需停止 (serial=%s)", self._device.serial)
|
|
151
|
+
return
|
|
152
|
+
for pid in pids:
|
|
153
|
+
try:
|
|
154
|
+
self._device.shell("kill -9 %s" % pid, timeout=5)
|
|
155
|
+
except DeviceConnectError as e:
|
|
156
|
+
logger.warn("kill -9 pid=%s 失败 (serial=%s): %s",
|
|
157
|
+
pid, self._device.serial, e)
|
|
158
|
+
# 确认进程已退出,避免端口占用影响下次启动
|
|
159
|
+
if self._is_daemon_running():
|
|
160
|
+
logger.warn("uitest 守护进程仍存活 (serial=%s)", self._device.serial)
|
|
161
|
+
else:
|
|
162
|
+
logger.info("uitest 守护进程已停止 (serial=%s)",
|
|
163
|
+
self._device.serial)
|
|
164
|
+
|
|
165
|
+
# ============================================================
|
|
166
|
+
# 设备信息探测
|
|
167
|
+
# ============================================================
|
|
168
|
+
|
|
169
|
+
def _detect_abi(self) -> str:
|
|
170
|
+
"""获取设备 CPU ABI。"""
|
|
171
|
+
try:
|
|
172
|
+
raw = self._device.shell(
|
|
173
|
+
'param get "const.product.cpu.abilist"', timeout=10
|
|
174
|
+
).strip()
|
|
175
|
+
except DeviceConnectError as e:
|
|
176
|
+
raise AgentError("获取设备 ABI 失败: %s" % e) from e
|
|
177
|
+
# abilist 可能是 "arm64-v8a,armeabi-v7a",取首个
|
|
178
|
+
abi = raw.split(",")[0].strip() if raw else ""
|
|
179
|
+
if not abi or abi == "default":
|
|
180
|
+
abi = "arm64-v8a"
|
|
181
|
+
return abi
|
|
182
|
+
|
|
183
|
+
def _detect_protocol_version(self) -> int:
|
|
184
|
+
"""获取 uitest 版本并判定协议版本。"""
|
|
185
|
+
try:
|
|
186
|
+
raw = self._device.shell("uitest --version", timeout=10)
|
|
187
|
+
except DeviceConnectError as e:
|
|
188
|
+
raise AgentError("获取 uitest 版本失败: %s" % e) from e
|
|
189
|
+
match = re.search(r"\d+\.\d+\.\d+\.\d+", raw)
|
|
190
|
+
if not match:
|
|
191
|
+
logger.warn("无法解析 uitest 版本 [%s],默认 v1", raw.strip())
|
|
192
|
+
return 1
|
|
193
|
+
uitest_version = match.group()
|
|
194
|
+
return (2 if _compare_version(uitest_version, PROTOCOL_V2_THRESHOLD) > 0
|
|
195
|
+
else 1)
|
|
196
|
+
|
|
197
|
+
# ============================================================
|
|
198
|
+
# agent.so 选择与推送
|
|
199
|
+
# ============================================================
|
|
200
|
+
|
|
201
|
+
def _select_agent_so(self, abi: str, protocol_version: int) -> str:
|
|
202
|
+
"""选择本地 agent.so 文件路径。"""
|
|
203
|
+
version_map = _AGENT_FILE_MAP.get(abi)
|
|
204
|
+
if version_map is None:
|
|
205
|
+
raise AgentError("不支持的设备 ABI: %s" % abi)
|
|
206
|
+
filename = version_map.get(protocol_version)
|
|
207
|
+
if filename is None:
|
|
208
|
+
raise AgentError(
|
|
209
|
+
"ABI %s 无 protocol v%d 对应的 agent.so" % (abi, protocol_version))
|
|
210
|
+
path = os.path.join(_get_package_root(), SO_ASSET_DIR, abi, filename)
|
|
211
|
+
if not os.path.isfile(path):
|
|
212
|
+
raise AgentError("本地 agent.so 不存在: %s" % path)
|
|
213
|
+
return path
|
|
214
|
+
|
|
215
|
+
def _deploy_agent_so(self, abi: str, protocol_version: int) -> None:
|
|
216
|
+
"""推送 agent.so 到设备,MD5 匹配则跳过。"""
|
|
217
|
+
local_path = self._select_agent_so(abi, protocol_version)
|
|
218
|
+
local_md5 = self._calc_file_md5(local_path)
|
|
219
|
+
|
|
220
|
+
device_md5 = self._get_device_file_md5(DEVICE_AGENT_PATH)
|
|
221
|
+
if device_md5 == local_md5:
|
|
222
|
+
logger.debug("agent.so MD5 匹配,跳过推送 (serial=%s)",
|
|
223
|
+
self._device.serial)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
logger.info("推送 agent.so (abi=%s, v%d, %s -> %s)",
|
|
227
|
+
abi, protocol_version,
|
|
228
|
+
(device_md5 or "none")[:8], local_md5[:8])
|
|
229
|
+
# 推送前删除旧文件,避免权限问题导致覆盖失败
|
|
230
|
+
try:
|
|
231
|
+
self._device.shell('rm -f "%s"' % DEVICE_AGENT_PATH, timeout=10)
|
|
232
|
+
except DeviceConnectError:
|
|
233
|
+
pass
|
|
234
|
+
self._device.push(local_path, DEVICE_AGENT_PATH)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _calc_file_md5(path: str) -> str:
|
|
238
|
+
"""计算本地文件 MD5。"""
|
|
239
|
+
h = hashlib.md5()
|
|
240
|
+
with open(path, "rb") as f:
|
|
241
|
+
for chunk in iter(lambda: f.read(64 * 1024), b""):
|
|
242
|
+
h.update(chunk)
|
|
243
|
+
return h.hexdigest()
|
|
244
|
+
|
|
245
|
+
def _get_device_file_md5(self, remote_path: str) -> Optional[str]:
|
|
246
|
+
"""获取设备端文件 MD5,不存在返回 None。"""
|
|
247
|
+
cmd = 'test -f %s && md5sum %s || echo NOT_EXISTS' % (
|
|
248
|
+
shlex.quote(remote_path), shlex.quote(remote_path))
|
|
249
|
+
try:
|
|
250
|
+
out = self._device.shell(cmd, timeout=15)
|
|
251
|
+
except DeviceConnectError:
|
|
252
|
+
return None
|
|
253
|
+
out = out.strip()
|
|
254
|
+
if not out or out == "NOT_EXISTS":
|
|
255
|
+
return None
|
|
256
|
+
match = re.match(r"^([a-f0-9]{32})", out, re.IGNORECASE)
|
|
257
|
+
return match.group(1).lower() if match else None
|
|
258
|
+
|
|
259
|
+
# ============================================================
|
|
260
|
+
# 守护进程管理
|
|
261
|
+
# ============================================================
|
|
262
|
+
|
|
263
|
+
def _is_daemon_running(self) -> bool:
|
|
264
|
+
"""检查 uitest 守护进程是否运行。
|
|
265
|
+
|
|
266
|
+
过滤扩展进程与 pgrep 自身:scrcpy_server、extension-name 等扩展
|
|
267
|
+
进程的 cmdline 可能包含 uitest 关键字;pgrep 命令自身的 shell
|
|
268
|
+
进程 cmdline 含匹配模式字符串,均会导致误判为守护进程已运行。
|
|
269
|
+
"""
|
|
270
|
+
cmd = ('pgrep -fl "uitest.*start-daemon.*singleness" '
|
|
271
|
+
'| grep -v "extension-name" | grep -v "scrcpy_server" '
|
|
272
|
+
'| grep -v "pgrep"')
|
|
273
|
+
try:
|
|
274
|
+
out = self._device.shell(cmd, timeout=10)
|
|
275
|
+
except DeviceConnectError:
|
|
276
|
+
return False
|
|
277
|
+
return bool(out.strip())
|
|
278
|
+
|
|
279
|
+
def _start_daemon_and_wait(self) -> None:
|
|
280
|
+
"""启动守护进程并渐进式等待就绪。"""
|
|
281
|
+
try:
|
|
282
|
+
self._device.shell(DAEMON_COMMAND, timeout=30)
|
|
283
|
+
except DeviceConnectError as e:
|
|
284
|
+
raise AgentError("启动 uitest 守护进程失败: %s" % e) from e
|
|
285
|
+
|
|
286
|
+
for delay in DAEMON_START_DELAYS:
|
|
287
|
+
time.sleep(delay)
|
|
288
|
+
if self._is_daemon_running():
|
|
289
|
+
logger.info("uitest 守护进程已启动 (serial=%s)",
|
|
290
|
+
self._device.serial)
|
|
291
|
+
# 进程已加载到内存,清理设备端临时 so 避免残留与版本混淆
|
|
292
|
+
try:
|
|
293
|
+
self._device.shell('rm -f "%s"' % DEVICE_AGENT_PATH,
|
|
294
|
+
timeout=5)
|
|
295
|
+
except DeviceConnectError:
|
|
296
|
+
pass
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
raise AgentError(
|
|
300
|
+
"uitest 守护进程启动超时 (serial=%s)" % self._device.serial)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""HarmonyDriverConfig:鸿蒙驱动配置。"""
|
|
4
|
+
from dataclasses import dataclass, replace
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from devhelmkit.exceptions import DevhelmError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComponentFindMode:
|
|
11
|
+
"""控件查找模式(类常量风格)。"""
|
|
12
|
+
UITEST = "uitest"
|
|
13
|
+
UITREE = "uitree"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClearTextMode:
|
|
17
|
+
"""清空文本模式。"""
|
|
18
|
+
ONCE = "once"
|
|
19
|
+
ONE_BY_ONE = "one_by_one"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InputTextModeConstant:
|
|
23
|
+
"""输入文本模式(PASTE 模式需设备端 uitest 支持,API level > 20)。"""
|
|
24
|
+
DEFAULT = "default"
|
|
25
|
+
PASTE = "paste"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StepLogMode:
|
|
29
|
+
"""步骤日志模式。"""
|
|
30
|
+
AUTO = "auto"
|
|
31
|
+
ENABLE = "enable"
|
|
32
|
+
DISABLE = "disable"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PopWindowHandlerConfig:
|
|
36
|
+
"""弹窗消除开关(pop_window_dismiss 字段取值)。"""
|
|
37
|
+
ENABLE = "enable"
|
|
38
|
+
DISABLE = "disable"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class HarmonyDriverConfig:
|
|
43
|
+
"""鸿蒙驱动配置,共 25 个配置项。
|
|
44
|
+
|
|
45
|
+
控制鸿蒙驱动行为:控件查找策略、弹窗处理、操作等待、文本输入、
|
|
46
|
+
截图、日志、显示、调试、模板匹配、扩展能力、资源清理、hdc 路径。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# 控件查找
|
|
50
|
+
component_find_backend: str = "uitest"
|
|
51
|
+
|
|
52
|
+
# 弹窗消除
|
|
53
|
+
pop_window_dismiss: str = "disable"
|
|
54
|
+
enable_pop_window_dismiss_in_check: bool = True
|
|
55
|
+
pop_window_retry_find_timeout: int = 2
|
|
56
|
+
wait_time_after_pop_window_dismiss: int = 1
|
|
57
|
+
preprocess_pop_window: bool = False
|
|
58
|
+
pop_window_handle_times: int = 4
|
|
59
|
+
pop_window_preprocess_times: int = 4
|
|
60
|
+
pop_window_service_restart_times: int = 3
|
|
61
|
+
enable_pop_window_screenshot: bool = True
|
|
62
|
+
|
|
63
|
+
# 操作等待
|
|
64
|
+
after_action_wait_time: int = 1
|
|
65
|
+
|
|
66
|
+
# 文本输入
|
|
67
|
+
clear_text_mode: str = ClearTextMode.ONCE
|
|
68
|
+
clear_text_before_input: bool = True
|
|
69
|
+
input_text_mode: str = InputTextModeConstant.DEFAULT
|
|
70
|
+
_out_of_screen_coord_for_input_text: Tuple[int, int] = (10000, 10000)
|
|
71
|
+
|
|
72
|
+
# 截图
|
|
73
|
+
enable_component_found_screenshot: bool = False
|
|
74
|
+
enable_action_screenshot: bool = False
|
|
75
|
+
screenshot_retry_times: int = 3
|
|
76
|
+
|
|
77
|
+
# 日志
|
|
78
|
+
save_step_log: str = StepLogMode.AUTO
|
|
79
|
+
|
|
80
|
+
# 显示
|
|
81
|
+
default_display_id: int = 0
|
|
82
|
+
|
|
83
|
+
# 调试
|
|
84
|
+
debug_page_info: bool = True
|
|
85
|
+
|
|
86
|
+
# 模板匹配
|
|
87
|
+
template_scale_range: Tuple[float, float] = (0.2, 1.2)
|
|
88
|
+
|
|
89
|
+
# 扩展
|
|
90
|
+
enable_extension: bool = True
|
|
91
|
+
|
|
92
|
+
# 资源清理
|
|
93
|
+
# close() 时是否停止设备端 uitest 守护进程,默认 False 复用进程
|
|
94
|
+
stop_daemon_on_close: bool = False
|
|
95
|
+
|
|
96
|
+
# hdc 可执行文件路径,默认 "hdc"(从 PATH 查找)
|
|
97
|
+
# 指定后 connect 时通过 HdcDevice.set_hdc_path 设置全局路径
|
|
98
|
+
hdc_path: str = "hdc"
|
|
99
|
+
|
|
100
|
+
def update_from_dict(self, data: dict) -> None:
|
|
101
|
+
"""从字典更新配置,未知字段抛 DevhelmError。"""
|
|
102
|
+
for key, value in data.items():
|
|
103
|
+
if not hasattr(self, key):
|
|
104
|
+
raise DevhelmError("未知配置字段: %s" % key)
|
|
105
|
+
setattr(self, key, value)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, data: dict) -> 'HarmonyDriverConfig':
|
|
109
|
+
"""从字典创建配置。"""
|
|
110
|
+
config = cls()
|
|
111
|
+
config.update_from_dict(data)
|
|
112
|
+
return config
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_input(cls, config: Optional['HarmonyDriverConfig'],
|
|
116
|
+
kwargs: dict) -> 'HarmonyDriverConfig':
|
|
117
|
+
"""合并显式配置对象和 connect 透传参数。
|
|
118
|
+
|
|
119
|
+
先复制 config,再用 kwargs 覆盖同名字段,原配置对象不被原地修改。
|
|
120
|
+
"""
|
|
121
|
+
merged = replace(config) if config is not None else cls()
|
|
122
|
+
if kwargs:
|
|
123
|
+
merged.update_from_dict(kwargs)
|
|
124
|
+
return merged
|