applib-ui 0.0.1__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.
- applib_ui/__init__.py +13 -0
- applib_ui/case.py +79 -0
- applib_ui/cli.py +75 -0
- applib_ui/core/__init__.py +0 -0
- applib_ui/core/android/__init__.py +16 -0
- applib_ui/core/android/case.py +8 -0
- applib_ui/core/android/driver.py +191 -0
- applib_ui/core/android/element.py +291 -0
- applib_ui/core/android/page.py +6 -0
- applib_ui/core/android/recorder.py +171 -0
- applib_ui/core/android/remote_adb.py +69 -0
- applib_ui/core/android/remote_driver.py +176 -0
- applib_ui/core/img/__init__.py +4 -0
- applib_ui/core/img/driver.py +43 -0
- applib_ui/core/img/element.py +73 -0
- applib_ui/core/ios/__init__.py +16 -0
- applib_ui/core/ios/case.py +8 -0
- applib_ui/core/ios/driver.py +290 -0
- applib_ui/core/ios/elem.py +222 -0
- applib_ui/core/ios/local_wda.py +576 -0
- applib_ui/core/ios/page.py +6 -0
- applib_ui/core/ios/recorder.py +202 -0
- applib_ui/core/ios/remote_driver.py +220 -0
- applib_ui/core/ios/remote_wda.py +54 -0
- applib_ui/core/ocr/__init__.py +5 -0
- applib_ui/core/ocr/driver.py +185 -0
- applib_ui/core/ocr/element.py +65 -0
- applib_ui/device.py +53 -0
- applib_ui/element.py +210 -0
- applib_ui/page.py +6 -0
- applib_ui/pytest_plugin.py +57 -0
- applib_ui/running/__init__.py +0 -0
- applib_ui/running/conf.py +14 -0
- applib_ui/running/conf.yml +19 -0
- applib_ui/running/runner.py +33 -0
- applib_ui/utils/__init__.py +1 -0
- applib_ui/utils/aes_util.py +47 -0
- applib_ui/utils/allure_util.py +213 -0
- applib_ui/utils/base64_util.py +21 -0
- applib_ui/utils/common.py +84 -0
- applib_ui/utils/config.py +135 -0
- applib_ui/utils/cv_util.py +110 -0
- applib_ui/utils/dingtalk.py +56 -0
- applib_ui/utils/excel_util.py +101 -0
- applib_ui/utils/log.py +12 -0
- applib_ui/utils/mail.py +47 -0
- applib_ui/utils/mysql_util.py +74 -0
- applib_ui/utils/nacos_util.py +52 -0
- applib_ui/utils/pytest_util.py +89 -0
- applib_ui/utils/swagger_util.py +195 -0
- applib_ui-0.0.1.dist-info/METADATA +170 -0
- applib_ui-0.0.1.dist-info/RECORD +55 -0
- applib_ui-0.0.1.dist-info/WHEEL +5 -0
- applib_ui-0.0.1.dist-info/entry_points.txt +5 -0
- applib_ui-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
基于本机 scrcpy 命令行的安卓录屏。
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import allure
|
|
12
|
+
|
|
13
|
+
from applib_ui.utils.log import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_scrcpy():
|
|
17
|
+
path = shutil.which("scrcpy")
|
|
18
|
+
if path:
|
|
19
|
+
return path
|
|
20
|
+
for candidate in ("/opt/homebrew/bin/scrcpy", "/usr/local/bin/scrcpy"):
|
|
21
|
+
if os.path.isfile(candidate):
|
|
22
|
+
return candidate
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ScrcpyRecorder:
|
|
27
|
+
#: 启动失败后重启 scrcpy 的重试次数(adb 抖动、设备被占用等偶发问题)
|
|
28
|
+
START_RETRY = 2
|
|
29
|
+
|
|
30
|
+
def __init__(self, serial: str):
|
|
31
|
+
self.serial = serial
|
|
32
|
+
self.file_path = None
|
|
33
|
+
self.proc = None
|
|
34
|
+
self._log_file = None
|
|
35
|
+
|
|
36
|
+
def start(self, file_path: str, timeout: float = 10) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
启动录屏:等录屏文件真正开始写数据才算成功,失败会重启 scrcpy 重试
|
|
39
|
+
@param file_path: 输出文件
|
|
40
|
+
@param timeout: 单次等待"文件开始写入"的上限
|
|
41
|
+
@return: 是否成功启动;最终失败只记日志不抛异常(录屏是辅助能力,不该让功能用例失败)
|
|
42
|
+
"""
|
|
43
|
+
scrcpy_bin = _find_scrcpy()
|
|
44
|
+
if not scrcpy_bin:
|
|
45
|
+
raise FileNotFoundError(
|
|
46
|
+
"未找到 scrcpy,请先执行: brew install scrcpy"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.file_path = file_path
|
|
50
|
+
base_cmd = [
|
|
51
|
+
scrcpy_bin,
|
|
52
|
+
"-s", self.serial,
|
|
53
|
+
"--record", file_path,
|
|
54
|
+
"--no-playback",
|
|
55
|
+
"--no-audio",
|
|
56
|
+
]
|
|
57
|
+
last_error = ""
|
|
58
|
+
for attempt in range(1, self.START_RETRY + 1):
|
|
59
|
+
logger.info(f"开始录屏({attempt}/{self.START_RETRY}): {file_path}")
|
|
60
|
+
# 每次重试前清掉上一次的残留文件,否则"文件非空"的判断会被旧文件骗过去
|
|
61
|
+
if os.path.exists(file_path):
|
|
62
|
+
os.remove(file_path)
|
|
63
|
+
try:
|
|
64
|
+
self._launch(base_cmd, timeout)
|
|
65
|
+
logger.info(f"录屏已开始: {file_path}")
|
|
66
|
+
return True
|
|
67
|
+
except RuntimeError as e:
|
|
68
|
+
last_error = str(e)
|
|
69
|
+
logger.warning(f"第 {attempt}/{self.START_RETRY} 次启动录屏失败:{last_error}")
|
|
70
|
+
# 清掉半死的进程,避免多个 scrcpy 抢同一个设备
|
|
71
|
+
self._terminate(timeout=5)
|
|
72
|
+
if attempt < self.START_RETRY:
|
|
73
|
+
time.sleep(0.5)
|
|
74
|
+
|
|
75
|
+
logger.error(f"scrcpy 录屏没起来,本次不会生成录屏文件:{last_error}")
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def _launch(self, base_cmd, timeout: float):
|
|
79
|
+
"""起 scrcpy 并等它真的开始写文件(含 --no-window 不被支持时的回退)"""
|
|
80
|
+
self.proc = self._popen(base_cmd + ["--no-window"])
|
|
81
|
+
time.sleep(0.3)
|
|
82
|
+
if self.proc.poll() is not None:
|
|
83
|
+
log = self._read_log()
|
|
84
|
+
if "unknown" in log.lower() or "unrecognized" in log.lower():
|
|
85
|
+
logger.warning("当前 scrcpy 不支持 --no-window,回退后再启动")
|
|
86
|
+
self.proc = self._popen(base_cmd)
|
|
87
|
+
else:
|
|
88
|
+
raise RuntimeError(f"scrcpy 启动失败: {log}")
|
|
89
|
+
|
|
90
|
+
deadline = time.time() + timeout
|
|
91
|
+
while time.time() < deadline:
|
|
92
|
+
if self.proc.poll() is not None:
|
|
93
|
+
raise RuntimeError(f"scrcpy 启动失败: {self._read_log()}")
|
|
94
|
+
if os.path.exists(self.file_path) and os.path.getsize(self.file_path) > 0:
|
|
95
|
+
return
|
|
96
|
+
time.sleep(0.2)
|
|
97
|
+
raise RuntimeError(f"等待录屏文件写入超时({timeout}s)")
|
|
98
|
+
|
|
99
|
+
def _popen(self, cmd):
|
|
100
|
+
self._close_log()
|
|
101
|
+
self._log_file = tempfile.TemporaryFile()
|
|
102
|
+
return subprocess.Popen(
|
|
103
|
+
cmd,
|
|
104
|
+
stdin=subprocess.DEVNULL,
|
|
105
|
+
stdout=self._log_file,
|
|
106
|
+
stderr=subprocess.STDOUT,
|
|
107
|
+
start_new_session=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _read_log(self):
|
|
111
|
+
if self._log_file is None:
|
|
112
|
+
return ""
|
|
113
|
+
self._log_file.seek(0)
|
|
114
|
+
return self._log_file.read().decode("utf-8", errors="ignore").strip()
|
|
115
|
+
|
|
116
|
+
def _close_log(self):
|
|
117
|
+
if self._log_file is not None:
|
|
118
|
+
self._log_file.close()
|
|
119
|
+
self._log_file = None
|
|
120
|
+
|
|
121
|
+
def _signal_group(self, sig):
|
|
122
|
+
if self.proc is None:
|
|
123
|
+
return
|
|
124
|
+
try:
|
|
125
|
+
os.killpg(self.proc.pid, sig)
|
|
126
|
+
except ProcessLookupError:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
def _terminate(self, timeout: float = 15):
|
|
130
|
+
"""SIGINT → SIGTERM → SIGKILL,逐级结束 scrcpy 进程组"""
|
|
131
|
+
if self.proc is None or self.proc.poll() is not None:
|
|
132
|
+
return
|
|
133
|
+
self._signal_group(signal.SIGINT)
|
|
134
|
+
try:
|
|
135
|
+
self.proc.wait(timeout=timeout)
|
|
136
|
+
return
|
|
137
|
+
except subprocess.TimeoutExpired:
|
|
138
|
+
logger.warning("scrcpy SIGINT 未退出,尝试 SIGTERM")
|
|
139
|
+
self._signal_group(signal.SIGTERM)
|
|
140
|
+
try:
|
|
141
|
+
self.proc.wait(timeout=5)
|
|
142
|
+
return
|
|
143
|
+
except subprocess.TimeoutExpired:
|
|
144
|
+
logger.error("scrcpy 停止超时,强制结束进程")
|
|
145
|
+
self._signal_group(signal.SIGKILL)
|
|
146
|
+
try:
|
|
147
|
+
self.proc.wait(timeout=5)
|
|
148
|
+
except subprocess.TimeoutExpired:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def stop(self):
|
|
152
|
+
self._terminate(timeout=15)
|
|
153
|
+
if self.proc is not None and self.proc.poll() not in (0, None, -signal.SIGINT, -signal.SIGTERM):
|
|
154
|
+
logger.error(f"scrcpy 退出异常(returncode={self.proc.returncode}): {self._read_log()}")
|
|
155
|
+
# 关掉日志前留一份,便于定位"为什么没录上"
|
|
156
|
+
scrcpy_log = self._read_log()
|
|
157
|
+
self.proc = None
|
|
158
|
+
self._close_log()
|
|
159
|
+
|
|
160
|
+
if self.file_path and os.path.exists(self.file_path) and os.path.getsize(self.file_path) > 0:
|
|
161
|
+
logger.info(f"录屏保存至: {self.file_path}")
|
|
162
|
+
logger.info("录屏上传allure报告")
|
|
163
|
+
allure.attach.file(
|
|
164
|
+
self.file_path,
|
|
165
|
+
attachment_type=allure.attachment_type.MP4,
|
|
166
|
+
name=self.file_path,
|
|
167
|
+
)
|
|
168
|
+
return self.file_path
|
|
169
|
+
|
|
170
|
+
logger.error("录屏文件为空或不存在" + (f";scrcpy 日志: {scrcpy_log[-300:]}" if scrcpy_log else ""))
|
|
171
|
+
return None
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from applib_ui.utils.common import get_free_port
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RemoteAdbInit:
|
|
7
|
+
"""
|
|
8
|
+
远端adb连接初始化
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, serial: str, sonic_host: str, sonic_user: str, sonic_pwd: str):
|
|
12
|
+
self.serial = serial
|
|
13
|
+
self.host = sonic_host
|
|
14
|
+
# 登录获取登录态请求头
|
|
15
|
+
print('sonic登录,获取token')
|
|
16
|
+
body = {"userName": sonic_user, "password": sonic_pwd}
|
|
17
|
+
res = requests.post(self.host + '/server/api/controller/users/login', json=body)
|
|
18
|
+
print(res.text)
|
|
19
|
+
token = res.json()['data']
|
|
20
|
+
self.headers = {
|
|
21
|
+
'sonictoken': token
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def occupy_device(self):
|
|
25
|
+
print('占用设备')
|
|
26
|
+
try:
|
|
27
|
+
body = {
|
|
28
|
+
"udId": self.serial,
|
|
29
|
+
"sasRemotePort": get_free_port(),
|
|
30
|
+
}
|
|
31
|
+
res = requests.post(self.host + '/server/api/controller/devices/occupy', json=body, headers=self.headers)
|
|
32
|
+
print(res.text)
|
|
33
|
+
res_json = res.json()
|
|
34
|
+
data = res_json['data']
|
|
35
|
+
print(data)
|
|
36
|
+
# uia2 = data['uia2']
|
|
37
|
+
sas = data['sas']
|
|
38
|
+
adb_server = sas.split()[-1]
|
|
39
|
+
return adb_server
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f'设备占用失败: \n + {str(e)}')
|
|
42
|
+
|
|
43
|
+
def release_device(self):
|
|
44
|
+
print('释放设备')
|
|
45
|
+
try:
|
|
46
|
+
query = {'udId': self.serial}
|
|
47
|
+
res = requests.get(self.host + '/server/api/controller/devices/release', params=query, headers=self.headers)
|
|
48
|
+
print(res.json())
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f'设备释放失败:\n + {str(e)}')
|
|
51
|
+
|
|
52
|
+
def delete_offline_device(self):
|
|
53
|
+
print('删除离线设备')
|
|
54
|
+
try:
|
|
55
|
+
# 查询离线设备
|
|
56
|
+
res = requests.get(f'{self.host}/server/api/controller/devices/list?page=1&pageSize=12&status%5B%5D=DISCONNECTED', headers=self.headers)
|
|
57
|
+
print(res.text)
|
|
58
|
+
res_json = res.json()
|
|
59
|
+
device_list = res_json['data']['content']
|
|
60
|
+
# 删除离线设备
|
|
61
|
+
for device in device_list:
|
|
62
|
+
device_id = device['id']
|
|
63
|
+
res = requests.delete(f'{self.host}/server/api/controller/devices?id={device_id}', headers=self.headers)
|
|
64
|
+
print(res.text)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"删除设备失败: {str(e)}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import allure
|
|
2
|
+
import time
|
|
3
|
+
import uiautomator2 as u2
|
|
4
|
+
|
|
5
|
+
from adbutils import adb
|
|
6
|
+
from .remote_adb import RemoteAdbInit
|
|
7
|
+
from applib_ui.utils.log import logger
|
|
8
|
+
from applib_ui.utils.common import general_file_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RemoteDriver:
|
|
12
|
+
"""
|
|
13
|
+
https://github.com/openatx/adbutils
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
serial: str,
|
|
19
|
+
sonic_host: str,
|
|
20
|
+
sonic_user: str,
|
|
21
|
+
sonic_pwd: str,
|
|
22
|
+
package: str = None
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
@param serial: 通过adb devices或者关于手机-序列号获取
|
|
26
|
+
@param package: 通过adb shell pm list packages获取
|
|
27
|
+
"""
|
|
28
|
+
logger.info(f'初始化远端设备连接: {serial}')
|
|
29
|
+
self.serial = serial
|
|
30
|
+
print('获取adb连接ip:port')
|
|
31
|
+
self.remote_adb = RemoteAdbInit(serial, sonic_host, sonic_user, sonic_pwd)
|
|
32
|
+
self.adb_server = self.remote_adb.occupy_device()
|
|
33
|
+
print(self.adb_server)
|
|
34
|
+
self.package = package
|
|
35
|
+
print('连接远端adb服务')
|
|
36
|
+
count = 15
|
|
37
|
+
while count > 0:
|
|
38
|
+
output = adb.connect(self.adb_server)
|
|
39
|
+
print(output)
|
|
40
|
+
if 'connected' in output:
|
|
41
|
+
logger.info('adb服务已就绪')
|
|
42
|
+
break
|
|
43
|
+
else:
|
|
44
|
+
logger.info('adb服务未就绪,1s后重试!')
|
|
45
|
+
time.sleep(1)
|
|
46
|
+
else:
|
|
47
|
+
# 释放设备
|
|
48
|
+
self.remote_adb.release_device()
|
|
49
|
+
raise KeyError('adb服务连接超时!')
|
|
50
|
+
|
|
51
|
+
device = adb.device(self.adb_server)
|
|
52
|
+
self.d = u2.connect(device)
|
|
53
|
+
|
|
54
|
+
def close(self):
|
|
55
|
+
print('释放远端设备连接')
|
|
56
|
+
# 释放设备
|
|
57
|
+
self.remote_adb.release_device()
|
|
58
|
+
# 断开adb连接
|
|
59
|
+
output = adb.disconnect(self.adb_server)
|
|
60
|
+
print(output)
|
|
61
|
+
# 等待3s
|
|
62
|
+
time.sleep(3)
|
|
63
|
+
# 删除设备
|
|
64
|
+
self.remote_adb.delete_offline_device()
|
|
65
|
+
|
|
66
|
+
def uninstall_app(self, package: str = None):
|
|
67
|
+
"""
|
|
68
|
+
卸载应用
|
|
69
|
+
@return:
|
|
70
|
+
"""
|
|
71
|
+
if self.package is None:
|
|
72
|
+
if package is None:
|
|
73
|
+
raise KeyError('package不能为空')
|
|
74
|
+
else:
|
|
75
|
+
self.d.app_uninstall(package)
|
|
76
|
+
else:
|
|
77
|
+
self.d.app_uninstall(self.package)
|
|
78
|
+
|
|
79
|
+
def install_app(self, apk_url: str, package: str = None):
|
|
80
|
+
"""
|
|
81
|
+
安装应用
|
|
82
|
+
@return:
|
|
83
|
+
"""
|
|
84
|
+
if self.package is None:
|
|
85
|
+
if package is None:
|
|
86
|
+
raise KeyError('package不能为空')
|
|
87
|
+
else:
|
|
88
|
+
self.d.app_install(package)
|
|
89
|
+
else:
|
|
90
|
+
self.d.app_install(self.package)
|
|
91
|
+
self.d.app_install(apk_url)
|
|
92
|
+
|
|
93
|
+
def start_app(self, package: str = None):
|
|
94
|
+
"""
|
|
95
|
+
启动应用
|
|
96
|
+
@return:
|
|
97
|
+
"""
|
|
98
|
+
if self.package is None:
|
|
99
|
+
if package is None:
|
|
100
|
+
raise KeyError('package不能为空')
|
|
101
|
+
else:
|
|
102
|
+
self.d.app_start(package)
|
|
103
|
+
else:
|
|
104
|
+
self.d.app_start(self.package)
|
|
105
|
+
|
|
106
|
+
def stop_app(self, package: str = None):
|
|
107
|
+
"""
|
|
108
|
+
停止应用
|
|
109
|
+
@return:
|
|
110
|
+
"""
|
|
111
|
+
if self.package is None:
|
|
112
|
+
if package is None:
|
|
113
|
+
raise KeyError('package不能为空')
|
|
114
|
+
else:
|
|
115
|
+
self.d.app_stop(package)
|
|
116
|
+
else:
|
|
117
|
+
self.d.app_stop(self.package)
|
|
118
|
+
|
|
119
|
+
def shot(self, file_name=None):
|
|
120
|
+
"""
|
|
121
|
+
截图
|
|
122
|
+
@param file_name:
|
|
123
|
+
@return:
|
|
124
|
+
"""
|
|
125
|
+
file_path = general_file_path(file_name, self.serial)
|
|
126
|
+
logger.info(f"截图保存至: {file_path}")
|
|
127
|
+
self.d.screenshot(file_path)
|
|
128
|
+
|
|
129
|
+
logger.info("截图上传allure报告")
|
|
130
|
+
allure.attach.file(
|
|
131
|
+
file_path,
|
|
132
|
+
attachment_type=allure.attachment_type.PNG,
|
|
133
|
+
name=f"{file_path}",
|
|
134
|
+
)
|
|
135
|
+
return file_path
|
|
136
|
+
|
|
137
|
+
def click(self, x, y):
|
|
138
|
+
"""
|
|
139
|
+
点击坐标
|
|
140
|
+
@param x:
|
|
141
|
+
@param y:
|
|
142
|
+
@return:
|
|
143
|
+
"""
|
|
144
|
+
self.d.click(x, y)
|
|
145
|
+
|
|
146
|
+
def input(self, *args, **kwargs):
|
|
147
|
+
"""
|
|
148
|
+
输入
|
|
149
|
+
@param args:
|
|
150
|
+
@param kwargs:
|
|
151
|
+
@return:
|
|
152
|
+
"""
|
|
153
|
+
self.d.send_keys(*args, **kwargs)
|
|
154
|
+
|
|
155
|
+
def shell(self, *args, **kwargs):
|
|
156
|
+
"""
|
|
157
|
+
执行adb shell
|
|
158
|
+
@param args:
|
|
159
|
+
@param kwargs:
|
|
160
|
+
@return:
|
|
161
|
+
"""
|
|
162
|
+
self.d.shell(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
def unlock(self):
|
|
165
|
+
"""
|
|
166
|
+
解锁设备
|
|
167
|
+
@return:
|
|
168
|
+
"""
|
|
169
|
+
self.shell("input keyevent WAKEUP")
|
|
170
|
+
self.d.swipe(0.1, 0.9, 0.9, 0.1)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 'pip install opencv-python==4.6.0.66'
|
|
2
|
+
import os
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ImageDiscern:
|
|
8
|
+
def __init__(self, target_image, source_image, grade=0.8, gauss_num=111) -> None:
|
|
9
|
+
"""__init__ [处理图片识别]
|
|
10
|
+
Args:
|
|
11
|
+
target_image (str): 被识别的目标图片
|
|
12
|
+
grade (float, optional): 分数. Defaults to 0.8.
|
|
13
|
+
gauss_num (int, optional): 过滤值. Defaults to 111.
|
|
14
|
+
"""
|
|
15
|
+
self.source_image = source_image
|
|
16
|
+
self.target_image = target_image
|
|
17
|
+
if not os.path.exists(self.source_image):
|
|
18
|
+
raise FileNotFoundError(f"文件: {self.source_image} 不存在")
|
|
19
|
+
if not os.path.exists(self.target_image):
|
|
20
|
+
raise FileNotFoundError(f"文件: {self.target_image} 不存在")
|
|
21
|
+
|
|
22
|
+
self.grade = grade
|
|
23
|
+
self.gauss_num = gauss_num
|
|
24
|
+
|
|
25
|
+
# 降噪处理(高斯滤波)
|
|
26
|
+
def __coordinate(self, image):
|
|
27
|
+
return cv2.GaussianBlur(image, (self.gauss_num, self.gauss_num), 0)
|
|
28
|
+
|
|
29
|
+
# 获取坐标图片坐标
|
|
30
|
+
def get_coordinate(self):
|
|
31
|
+
# 这里不用imread,解决中文目录无法读取的问题
|
|
32
|
+
screen = cv2.imdecode(np.fromfile(self.source_image, dtype=np.uint8), 1)
|
|
33
|
+
target = cv2.imdecode(np.fromfile(self.target_image, dtype=np.uint8), 1)
|
|
34
|
+
result = cv2.matchTemplate(self.__coordinate(screen),
|
|
35
|
+
self.__coordinate(target),
|
|
36
|
+
cv2.TM_CCOEFF_NORMED)
|
|
37
|
+
min, max, min_loc, max_loc = cv2.minMaxLoc(result)
|
|
38
|
+
if max <= self.grade:
|
|
39
|
+
return None
|
|
40
|
+
else:
|
|
41
|
+
x = max_loc[0] + int(target.shape[1] / 2)
|
|
42
|
+
y = max_loc[1] + int(target.shape[0] / 2)
|
|
43
|
+
return (x, y)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 'pip install opencv-python==4.6.0.66'
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from .driver import ImageDiscern
|
|
5
|
+
from applib_ui.utils.log import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ImgElem(object):
|
|
9
|
+
"""图像识别定位"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, driver=None, file: str = None):
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@param driver: 设备驱动
|
|
15
|
+
@param file: 图片文件路径
|
|
16
|
+
"""
|
|
17
|
+
self.driver = driver
|
|
18
|
+
self.target_image = file
|
|
19
|
+
# if hasattr(self.driver, 'scale') is True:
|
|
20
|
+
# self.scale = self.driver.scale
|
|
21
|
+
# else:
|
|
22
|
+
# self.scale = None
|
|
23
|
+
|
|
24
|
+
def __get__(self, instance, owner):
|
|
25
|
+
if instance is None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
self.driver = instance.driver
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def wait(self, timeout=1):
|
|
32
|
+
logger.info(f'图像识别判断: {self.target_image} 是否存在')
|
|
33
|
+
time.sleep(timeout)
|
|
34
|
+
source_image = self.driver.shot("图像识别中")
|
|
35
|
+
res = ImageDiscern(self.target_image, source_image).get_coordinate()
|
|
36
|
+
logger.debug(res)
|
|
37
|
+
# logger.error(self.scale)
|
|
38
|
+
# logger.error(self.driver.d.scale)
|
|
39
|
+
# logger.error(self.driver.d.screen_info())
|
|
40
|
+
if isinstance(res, tuple):
|
|
41
|
+
x, y = res[0], res[1]
|
|
42
|
+
try:
|
|
43
|
+
scale = self.driver.d.scale
|
|
44
|
+
except:
|
|
45
|
+
scale = None
|
|
46
|
+
if scale is not None:
|
|
47
|
+
"""iphone的scale是3"""
|
|
48
|
+
x, y = int(x / scale), int(y / scale)
|
|
49
|
+
return x, y
|
|
50
|
+
else:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def click_exists(self, timeout=3):
|
|
54
|
+
logger.info(f'图像如果存在则点击: {self.target_image}')
|
|
55
|
+
res = self.wait(timeout=timeout)
|
|
56
|
+
if res is not False:
|
|
57
|
+
self.driver.click(*res)
|
|
58
|
+
# logger.info(f"点击({res})")
|
|
59
|
+
else:
|
|
60
|
+
logger.info("图像不存在,不进行点击")
|
|
61
|
+
|
|
62
|
+
def click(self, retry=3, timeout=3):
|
|
63
|
+
logger.info(f'图像识别点击: {self.target_image}')
|
|
64
|
+
for i in range(retry):
|
|
65
|
+
logger.info(f'第{i + 1}次查找:')
|
|
66
|
+
res = self.wait(timeout=timeout)
|
|
67
|
+
if res is not False:
|
|
68
|
+
self.driver.click(*res)
|
|
69
|
+
logger.info(f"点击{res}")
|
|
70
|
+
break
|
|
71
|
+
else:
|
|
72
|
+
self.driver.shot(f'图像识别定位失败')
|
|
73
|
+
raise Exception('未识别到图片,无法进行点击')
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""ios 平台接口,按需加载驱动依赖。"""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
__all__ = ['Elem', 'Page', 'Driver', 'RemoteDriver', 'TestCase']
|
|
6
|
+
|
|
7
|
+
_MODULES = {'Elem': 'elem', 'Page': 'page', 'Driver': 'driver',
|
|
8
|
+
'RemoteDriver': 'remote_driver', 'TestCase': 'case'}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def __getattr__(name):
|
|
12
|
+
if name not in _MODULES:
|
|
13
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
|
14
|
+
value = getattr(import_module(f'.{_MODULES[name]}', __name__), name)
|
|
15
|
+
globals()[name] = value
|
|
16
|
+
return value
|