Kea2-python 1.0.6b0__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.
Potentially problematic release.
This version of Kea2-python might be problematic. Click here for more details.
- kea2/__init__.py +3 -0
- kea2/absDriver.py +56 -0
- kea2/adbUtils.py +554 -0
- kea2/assets/config_version.json +16 -0
- kea2/assets/fastbot-thirdpart.jar +0 -0
- kea2/assets/fastbot_configs/abl.strings +2 -0
- kea2/assets/fastbot_configs/awl.strings +3 -0
- kea2/assets/fastbot_configs/max.config +7 -0
- kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
- kea2/assets/fastbot_configs/max.schema.strings +1 -0
- kea2/assets/fastbot_configs/max.strings +3 -0
- kea2/assets/fastbot_configs/max.tree.pruning +27 -0
- kea2/assets/fastbot_configs/teardown.py +18 -0
- kea2/assets/fastbot_configs/widget.block.py +38 -0
- kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
- kea2/assets/framework.jar +0 -0
- kea2/assets/kea2-thirdpart.jar +0 -0
- kea2/assets/monkeyq.jar +0 -0
- kea2/assets/quicktest.py +126 -0
- kea2/cli.py +320 -0
- kea2/fastbotManager.py +267 -0
- kea2/fastbotx/ActivityTimes.py +52 -0
- kea2/fastbotx/ReuseEntry.py +74 -0
- kea2/fastbotx/ReuseModel.py +63 -0
- kea2/fastbotx/__init__.py +7 -0
- kea2/fbm_parser.py +871 -0
- kea2/fs_lock.py +131 -0
- kea2/kea2_api.py +166 -0
- kea2/keaUtils.py +1112 -0
- kea2/kea_launcher.py +319 -0
- kea2/logWatcher.py +92 -0
- kea2/mixin.py +22 -0
- kea2/report/__init__.py +0 -0
- kea2/report/bug_report_generator.py +793 -0
- kea2/report/mixin.py +482 -0
- kea2/report/report_merger.py +797 -0
- kea2/report/templates/bug_report_template.html +3876 -0
- kea2/report/templates/merged_bug_report_template.html +3333 -0
- kea2/report/utils.py +10 -0
- kea2/resultSyncer.py +65 -0
- kea2/u2Driver.py +610 -0
- kea2/utils.py +184 -0
- kea2/version_manager.py +102 -0
- kea2_python-1.0.6b0.dist-info/METADATA +447 -0
- kea2_python-1.0.6b0.dist-info/RECORD +52 -0
- kea2_python-1.0.6b0.dist-info/WHEEL +5 -0
- kea2_python-1.0.6b0.dist-info/entry_points.txt +2 -0
- kea2_python-1.0.6b0.dist-info/licenses/LICENSE +16 -0
- kea2_python-1.0.6b0.dist-info/top_level.txt +1 -0
kea2/report/utils.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
|
|
4
|
+
@contextmanager
|
|
5
|
+
def thread_pool(max_workers=128, wait=True, name_prefix="worker"):
|
|
6
|
+
executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix=name_prefix)
|
|
7
|
+
try:
|
|
8
|
+
yield executor
|
|
9
|
+
finally:
|
|
10
|
+
executor.shutdown(wait=wait)
|
kea2/resultSyncer.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .adbUtils import ADBDevice
|
|
6
|
+
from .utils import getLogger, catchException, timer
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .keaUtils import Options
|
|
11
|
+
|
|
12
|
+
logger = getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResultSyncer:
|
|
16
|
+
|
|
17
|
+
def __init__(self, device_output_dir, options: "Options"):
|
|
18
|
+
self.device_output_dir = device_output_dir
|
|
19
|
+
self.output_dir = options.output_dir / Path(device_output_dir).name
|
|
20
|
+
self.running = False
|
|
21
|
+
self.thread = None
|
|
22
|
+
self.sync_event = threading.Event()
|
|
23
|
+
|
|
24
|
+
ADBDevice.setDevice(serial=options.serial, transport_id=options.transport_id)
|
|
25
|
+
self.dev = ADBDevice()
|
|
26
|
+
|
|
27
|
+
def run(self):
|
|
28
|
+
"""Start a background thread to sync device data when triggered"""
|
|
29
|
+
self.running = True
|
|
30
|
+
self.thread = threading.Thread(target=self._sync_thread, daemon=True)
|
|
31
|
+
self.thread.start()
|
|
32
|
+
|
|
33
|
+
def _sync_thread(self):
|
|
34
|
+
"""Thread function that waits for sync event and then syncs data"""
|
|
35
|
+
while self.running:
|
|
36
|
+
# Wait for sync event with a timeout to periodically check if still running
|
|
37
|
+
if self.sync_event.wait(timeout=1):
|
|
38
|
+
self._sync_device_data()
|
|
39
|
+
self.sync_event.clear()
|
|
40
|
+
|
|
41
|
+
@timer("Data Sync cost %cost_time seconds")
|
|
42
|
+
def close(self):
|
|
43
|
+
self.running = False
|
|
44
|
+
self.sync_event.set()
|
|
45
|
+
if self.thread and self.thread.is_alive():
|
|
46
|
+
logger.info("Syncing result data from device. Please wait...")
|
|
47
|
+
self.thread.join(timeout=10)
|
|
48
|
+
self._sync_device_data()
|
|
49
|
+
try:
|
|
50
|
+
logger.debug(f"Removing device output directory: {self.device_output_dir}")
|
|
51
|
+
remove_device_dir = ["rm", "-rf", self.device_output_dir]
|
|
52
|
+
self.dev.shell(remove_device_dir)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Error removing device output directory: {e}", flush=True)
|
|
55
|
+
|
|
56
|
+
@catchException("Error during device data sync.")
|
|
57
|
+
def _sync_device_data(self):
|
|
58
|
+
"""
|
|
59
|
+
Sync the device data to the local directory.
|
|
60
|
+
"""
|
|
61
|
+
logger.debug("Syncing data")
|
|
62
|
+
self.dev.sync.pull_dir(self.device_output_dir, self.output_dir, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
remove_pulled_screenshots = ["find", self.device_output_dir, "-name", '"*.png"', "-delete"]
|
|
65
|
+
self.dev.shell(remove_pulled_screenshots)
|
kea2/u2Driver.py
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from time import sleep
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
import uiautomator2 as u2
|
|
6
|
+
import adbutils
|
|
7
|
+
import types
|
|
8
|
+
import rtree
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from typing import List, Literal, Union, Optional
|
|
12
|
+
from lxml import etree
|
|
13
|
+
from packaging.version import Version
|
|
14
|
+
from .absDriver import AbstractScriptDriver, AbstractStaticChecker, AbstractDriver
|
|
15
|
+
from .adbUtils import list_forwards, remove_forward
|
|
16
|
+
from .utils import getLogger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
logging.getLogger("urllib3").setLevel(logging.INFO)
|
|
21
|
+
logging.getLogger("uiautomator2").setLevel(logging.INFO)
|
|
22
|
+
|
|
23
|
+
logger = getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
The definition of U2ScriptDriver
|
|
27
|
+
"""
|
|
28
|
+
class U2ScriptDriver(AbstractScriptDriver):
|
|
29
|
+
"""
|
|
30
|
+
This is the ScriptDriver used to send ui automation request in Property
|
|
31
|
+
When you interact with the mobile in properties. You will use the object here
|
|
32
|
+
|
|
33
|
+
*e.g. the following self.d use U2ScriptDriver*
|
|
34
|
+
```
|
|
35
|
+
@precondition(...)
|
|
36
|
+
def test_battery(self):
|
|
37
|
+
self.d(text="battery").click()
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
deviceSerial: str = None
|
|
42
|
+
transportId: str = None
|
|
43
|
+
d = None
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def setTransportId(cls, transportId):
|
|
47
|
+
cls.transportId = transportId
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def setDeviceSerial(cls, deviceSerial):
|
|
51
|
+
cls.deviceSerial = deviceSerial
|
|
52
|
+
|
|
53
|
+
def getInstance(self):
|
|
54
|
+
if self.d is None:
|
|
55
|
+
adb = adbutils.device(serial=self.deviceSerial, transport_id=self.transportId)
|
|
56
|
+
print("[INFO] Connecting to uiautomator2. Please wait ...")
|
|
57
|
+
self.d = u2.connect(adb)
|
|
58
|
+
sleep(5)
|
|
59
|
+
self.d._device_server_port = 8090
|
|
60
|
+
return self.d
|
|
61
|
+
|
|
62
|
+
def _remove_remote_port(self, port:int):
|
|
63
|
+
"""remove the forward port
|
|
64
|
+
"""
|
|
65
|
+
forwardLists = list_forwards(device=self.deviceSerial)
|
|
66
|
+
for forward in forwardLists:
|
|
67
|
+
if forward["remote"] == f"tcp:{port}":
|
|
68
|
+
forward_local = forward["local"]
|
|
69
|
+
remove_forward(local_spec=forward_local, device=self.deviceSerial)
|
|
70
|
+
|
|
71
|
+
def tearDown(self):
|
|
72
|
+
logger.debug("U2Driver tearDown: stop_uiautomator")
|
|
73
|
+
if self.d is None:
|
|
74
|
+
return
|
|
75
|
+
try:
|
|
76
|
+
self.d._device_server_port = 9008
|
|
77
|
+
self.d.stop_uiautomator()
|
|
78
|
+
except (OSError, AttributeError, RuntimeError) as e:
|
|
79
|
+
logger.debug(f"Error during uiautomator teardown (may be already closed): {e}")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(f"Unexpected error during uiautomator teardown: {e}")
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
The definition of U2StaticChecker
|
|
85
|
+
"""
|
|
86
|
+
class StaticU2UiObject(u2.UiObject):
|
|
87
|
+
def __init__(self, session, selector):
|
|
88
|
+
self.session: U2StaticDevice = session
|
|
89
|
+
self.selector = selector
|
|
90
|
+
|
|
91
|
+
def _transferU2Keys(self, originKey):
|
|
92
|
+
filterDict = {
|
|
93
|
+
"resourceId": "resource-id",
|
|
94
|
+
"description": "content-desc",
|
|
95
|
+
"className": "class",
|
|
96
|
+
"longClickable": "long-clickable",
|
|
97
|
+
}
|
|
98
|
+
if filterDict.get(originKey, None):
|
|
99
|
+
return filterDict[originKey]
|
|
100
|
+
return originKey
|
|
101
|
+
|
|
102
|
+
def selector_to_xpath(self, selector: u2.Selector, is_initial: bool = True) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
selector (u2.Selector): A u2 Selector object
|
|
108
|
+
is_initial (bool): Whether it is the initial node, defaults to True
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
str: The corresponding XPath expression
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
|
|
115
|
+
xpath = ".//node" if is_initial else "node"
|
|
116
|
+
|
|
117
|
+
conditions = []
|
|
118
|
+
|
|
119
|
+
if "className" in selector:
|
|
120
|
+
conditions.insert(0, f"[@class='{selector['className']}']")
|
|
121
|
+
|
|
122
|
+
if "text" in selector:
|
|
123
|
+
conditions.append(f"[@text='{selector['text']}']")
|
|
124
|
+
elif "textContains" in selector:
|
|
125
|
+
conditions.append(f"[contains(@text, '{selector['textContains']}')]")
|
|
126
|
+
elif "textStartsWith" in selector:
|
|
127
|
+
conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
|
|
128
|
+
elif "textMatches" in selector:
|
|
129
|
+
raise NotImplementedError("'textMatches' syntax is not supported")
|
|
130
|
+
|
|
131
|
+
if "description" in selector:
|
|
132
|
+
conditions.append(f"[@content-desc='{selector['description']}']")
|
|
133
|
+
elif "descriptionContains" in selector:
|
|
134
|
+
conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
|
|
135
|
+
elif "descriptionStartsWith" in selector:
|
|
136
|
+
conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
|
|
137
|
+
elif "descriptionMatches" in selector:
|
|
138
|
+
raise NotImplementedError("'descriptionMatches' syntax is not supported")
|
|
139
|
+
|
|
140
|
+
if "packageName" in selector:
|
|
141
|
+
conditions.append(f"[@package='{selector['packageName']}']")
|
|
142
|
+
elif "packageNameMatches" in selector:
|
|
143
|
+
raise NotImplementedError("'packageNameMatches' syntax is not supported")
|
|
144
|
+
|
|
145
|
+
if "resourceId" in selector:
|
|
146
|
+
conditions.append(f"[@resource-id='{selector['resourceId']}']")
|
|
147
|
+
elif "resourceIdMatches" in selector:
|
|
148
|
+
raise NotImplementedError("'resourceIdMatches' syntax is not supported")
|
|
149
|
+
|
|
150
|
+
bool_props = ["checkable", "checked", "clickable", "longClickable", "scrollable", "enabled", "focusable",
|
|
151
|
+
"focused", "selected", "covered"]
|
|
152
|
+
|
|
153
|
+
def str_to_bool(value):
|
|
154
|
+
"""Convert string 'true'/'false' to boolean, or return original value if already boolean"""
|
|
155
|
+
if isinstance(value, str):
|
|
156
|
+
return value.lower() == "true"
|
|
157
|
+
return bool(value)
|
|
158
|
+
|
|
159
|
+
for prop in bool_props:
|
|
160
|
+
if prop in selector:
|
|
161
|
+
bool_value = str_to_bool(selector[prop])
|
|
162
|
+
value = "true" if bool_value else "false"
|
|
163
|
+
conditions.append(f"[@{prop}='{value}']")
|
|
164
|
+
|
|
165
|
+
if "index" in selector:
|
|
166
|
+
conditions.append(f"[@index='{selector['index']}']")
|
|
167
|
+
|
|
168
|
+
xpath += "".join(conditions)
|
|
169
|
+
|
|
170
|
+
if "childOrSibling" in selector and selector["childOrSibling"]:
|
|
171
|
+
for i, relation in enumerate(selector["childOrSibling"]):
|
|
172
|
+
sub_selector = selector["childOrSiblingSelector"][i]
|
|
173
|
+
sub_xpath = self.selector_to_xpath(sub_selector, False)
|
|
174
|
+
|
|
175
|
+
if relation == "child":
|
|
176
|
+
xpath += f"//{sub_xpath}"
|
|
177
|
+
elif relation == "sibling":
|
|
178
|
+
cur_root = xpath
|
|
179
|
+
following_sibling = cur_root + f"/following-sibling::{sub_xpath}"
|
|
180
|
+
preceding_sibling = cur_root + f"/preceding-sibling::{sub_xpath}"
|
|
181
|
+
xpath = f"({following_sibling} | {preceding_sibling})"
|
|
182
|
+
if "instance" in selector:
|
|
183
|
+
xpath = f"({xpath})[{selector['instance'] + 1}]"
|
|
184
|
+
|
|
185
|
+
return xpath
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
print(f"Error occurred during selector conversion: {e}")
|
|
189
|
+
return "//error"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def exists(self):
|
|
194
|
+
set_covered_to_deepest_node(self.selector)
|
|
195
|
+
xpath = self.selector_to_xpath(self.selector)
|
|
196
|
+
matched_widgets = self.session.xml.xpath(xpath)
|
|
197
|
+
return bool(matched_widgets)
|
|
198
|
+
|
|
199
|
+
def __len__(self):
|
|
200
|
+
xpath = self.selector_to_xpath(self.selector)
|
|
201
|
+
matched_widgets = self.session.xml.xpath(xpath)
|
|
202
|
+
return len(matched_widgets)
|
|
203
|
+
|
|
204
|
+
def child(self, **kwargs):
|
|
205
|
+
return StaticU2UiObject(self.session, self.selector.clone().child(**kwargs))
|
|
206
|
+
|
|
207
|
+
def sibling(self, **kwargs):
|
|
208
|
+
return StaticU2UiObject(self.session, self.selector.clone().sibling(**kwargs))
|
|
209
|
+
|
|
210
|
+
def __getattr__(self, attr):
|
|
211
|
+
return getattr(super(), attr)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class StaticXpathObject(u2.xpath.XPathSelector):
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
The definition of XpathStaticChecker
|
|
219
|
+
"""
|
|
220
|
+
class StaticXpathObjectV1(StaticXpathObject):
|
|
221
|
+
def __init__(self, session, selector):
|
|
222
|
+
self.session: U2StaticDevice = session
|
|
223
|
+
self.selector = selector
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def exists(self):
|
|
227
|
+
source = self.session.get_page_source()
|
|
228
|
+
return len(self.selector.all(source)) > 0
|
|
229
|
+
|
|
230
|
+
def __and__(self, value) -> 'StaticXpathObject':
|
|
231
|
+
s = u2.xpath.XPathSelector(self.selector)
|
|
232
|
+
s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
|
|
233
|
+
s._operator = u2.xpath.Operator.AND
|
|
234
|
+
s._parent = self.selector._parent
|
|
235
|
+
self.selector = s
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
def __or__(self, value) -> 'StaticXpathObject':
|
|
239
|
+
s = u2.xpath.XPathSelector(self.selector)
|
|
240
|
+
s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
|
|
241
|
+
s._operator = u2.xpath.Operator.OR
|
|
242
|
+
s._parent = self.selector._parent
|
|
243
|
+
self.selector = s
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def selector_to_xpath(self, selector: u2.xpath.XPathSelector) -> str:
|
|
247
|
+
"""
|
|
248
|
+
Convert an XPathSelector to a standard XPath expression.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
selector: The XPathSelector object to convert.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
A standard XPath expression as a string.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def _handle_path(path):
|
|
258
|
+
if isinstance(path, u2.xpath.XPathSelector):
|
|
259
|
+
return self.selector_to_xpath(path)
|
|
260
|
+
elif isinstance(path, u2.xpath.XPath):
|
|
261
|
+
return str(path)
|
|
262
|
+
else:
|
|
263
|
+
return path
|
|
264
|
+
|
|
265
|
+
base_xpath = _handle_path(selector._base_xpath)
|
|
266
|
+
base_xpath = base_xpath.replace('//*', './/node')
|
|
267
|
+
|
|
268
|
+
if selector._operator is None:
|
|
269
|
+
return base_xpath
|
|
270
|
+
else:
|
|
271
|
+
print("Unsupported operator: {}".format(selector._operator))
|
|
272
|
+
return "//error"
|
|
273
|
+
|
|
274
|
+
def xpath(self, _xpath: Union[list, tuple, str]) -> 'StaticXpathObject':
|
|
275
|
+
"""
|
|
276
|
+
add xpath to condition list
|
|
277
|
+
the element should match all conditions
|
|
278
|
+
|
|
279
|
+
Deprecated, using a & b instead
|
|
280
|
+
"""
|
|
281
|
+
if isinstance(_xpath, (list, tuple)):
|
|
282
|
+
self.selector = functools.reduce(lambda a, b: a & b, _xpath, self)
|
|
283
|
+
else:
|
|
284
|
+
self.selector = self.selector & _xpath
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
def child(self, _xpath: str) -> "StaticXpathObject":
|
|
288
|
+
"""
|
|
289
|
+
add child xpath
|
|
290
|
+
"""
|
|
291
|
+
if self.selector._operator or not isinstance(self.selector._base_xpath, u2.xpath.XPath):
|
|
292
|
+
raise u2.xpath.XPathError("can't use child when base is not XPath or operator is set")
|
|
293
|
+
new = self.selector.copy()
|
|
294
|
+
new._base_xpath = self.selector._base_xpath.joinpath(_xpath)
|
|
295
|
+
self.selector = new
|
|
296
|
+
return self
|
|
297
|
+
|
|
298
|
+
def get(self, timeout=None) -> "u2.xpath.XMLElement":
|
|
299
|
+
"""
|
|
300
|
+
Get first matched element
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
timeout (float): max seconds to wait
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
XMLElement
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
if not self.exists:
|
|
310
|
+
return None
|
|
311
|
+
return self.get_last_match()
|
|
312
|
+
|
|
313
|
+
def get_last_match(self) -> "u2.xpath.XMLElement":
|
|
314
|
+
return self.selector.all(self.selector._last_source)[0]
|
|
315
|
+
|
|
316
|
+
def parent_exists(self, xpath: Optional[str] = None):
|
|
317
|
+
el = self.get()
|
|
318
|
+
if el is None:
|
|
319
|
+
return False
|
|
320
|
+
element = el.parent(xpath) if hasattr(el, 'parent') else None
|
|
321
|
+
return True if element is not None else False
|
|
322
|
+
|
|
323
|
+
def __getattr__(self, key: str):
|
|
324
|
+
"""
|
|
325
|
+
In IPython console, attr:_ipython_canary_method_should_not_exist_ will be called
|
|
326
|
+
So here ignore all attr startswith _
|
|
327
|
+
"""
|
|
328
|
+
if key.startswith("_"):
|
|
329
|
+
raise AttributeError("Invalid attr", key)
|
|
330
|
+
if not hasattr(u2.xpath.XMLElement, key):
|
|
331
|
+
raise AttributeError("Invalid attr", key)
|
|
332
|
+
return getattr(super(), key)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class StaticXpathObjectV2(StaticXpathObjectV1):
|
|
336
|
+
def __and__(self, value) -> 'StaticXpathObject':
|
|
337
|
+
s = u2.xpath.XPathSelector(self.selector)
|
|
338
|
+
s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
|
|
339
|
+
s._operator = u2.xpath.Operator.AND
|
|
340
|
+
self.selector = s
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
def __or__(self, value) -> 'StaticXpathObject':
|
|
344
|
+
s = u2.xpath.XPathSelector(self.selector)
|
|
345
|
+
s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
|
|
346
|
+
s._operator = u2.xpath.Operator.OR
|
|
347
|
+
self.selector = s
|
|
348
|
+
return self
|
|
349
|
+
|
|
350
|
+
def get_last_match(self) -> "u2.xpath.XMLElement":
|
|
351
|
+
source = self.session.get_page_source()
|
|
352
|
+
return self.selector.all(source)[0]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class StaticXpathUiObjectFactory:
|
|
356
|
+
|
|
357
|
+
_u2_version = None
|
|
358
|
+
|
|
359
|
+
@classmethod
|
|
360
|
+
def get_u2_version(cls):
|
|
361
|
+
if cls._u2_version is None:
|
|
362
|
+
cls._u2_version = Version(version("uiautomator2"))
|
|
363
|
+
return cls._u2_version
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def create(cls, session, xpath, source) -> StaticXpathObject:
|
|
367
|
+
if cls.get_u2_version() <= Version("3.4.0"):
|
|
368
|
+
return StaticXpathObjectV1(session, selector=u2.xpath.XPathSelector(xpath, source=source))
|
|
369
|
+
elif cls.get_u2_version() >= Version("3.4.1"):
|
|
370
|
+
return StaticXpathObjectV2(session, selector=u2.xpath.XPathSelector(xpath))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _get_bounds(raw_bounds):
|
|
374
|
+
pattern = re.compile(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]")
|
|
375
|
+
m = re.match(pattern, raw_bounds)
|
|
376
|
+
try:
|
|
377
|
+
bounds = [int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))]
|
|
378
|
+
except Exception as e:
|
|
379
|
+
print(f"raw_bounds: {raw_bounds}", flush=True)
|
|
380
|
+
print(f"Please report this bug to Kea2", flush=True)
|
|
381
|
+
raise RuntimeError(e)
|
|
382
|
+
|
|
383
|
+
return bounds
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class _HindenWidgetFilter:
|
|
387
|
+
def __init__(self, root: etree._Element):
|
|
388
|
+
# self.global_drawing_order = 0
|
|
389
|
+
self._nodes = []
|
|
390
|
+
|
|
391
|
+
self.idx = rtree.index.Index()
|
|
392
|
+
try:
|
|
393
|
+
self.set_covered_attr(root)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
import traceback, uuid
|
|
396
|
+
traceback.print_exc()
|
|
397
|
+
logger.error(f"Error in setting covered widgets")
|
|
398
|
+
from .utils import LoggingLevel
|
|
399
|
+
if LoggingLevel.level <= logging.DEBUG:
|
|
400
|
+
with open(f"kea2_error_tree_{uuid.uuid4().hex}.xml", "wb") as f:
|
|
401
|
+
xml_bytes = etree.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True)
|
|
402
|
+
f.write(xml_bytes)
|
|
403
|
+
|
|
404
|
+
# xml_bytes = etree.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True)
|
|
405
|
+
# with open("filtered_tree.xml", "wb") as f:
|
|
406
|
+
# f.write(xml_bytes)
|
|
407
|
+
# xml_bytes
|
|
408
|
+
|
|
409
|
+
def _iter_by_drawing_order(self, ele: etree._Element):
|
|
410
|
+
"""
|
|
411
|
+
iter by drawing order (DFS)
|
|
412
|
+
"""
|
|
413
|
+
if ele.tag == "node":
|
|
414
|
+
yield ele
|
|
415
|
+
|
|
416
|
+
children = list(ele)
|
|
417
|
+
try:
|
|
418
|
+
children.sort(key=lambda e: int(e.get("drawing-order", 0)))
|
|
419
|
+
except (TypeError, ValueError):
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
for child in children:
|
|
423
|
+
yield from self._iter_by_drawing_order(child)
|
|
424
|
+
|
|
425
|
+
def set_covered_attr(self, root: etree._Element):
|
|
426
|
+
self._nodes: List[etree._Element] = list()
|
|
427
|
+
for e in self._iter_by_drawing_order(root):
|
|
428
|
+
# e.set("global-order", str(self.global_drawing_order))
|
|
429
|
+
# self.global_drawing_order += 1
|
|
430
|
+
e.set("covered", "false")
|
|
431
|
+
|
|
432
|
+
# algorithm: filter by "clickable"
|
|
433
|
+
clickable = (e.get("clickable", "false") == "true")
|
|
434
|
+
_raw_bounds = e.get("bounds")
|
|
435
|
+
if _raw_bounds is None:
|
|
436
|
+
continue
|
|
437
|
+
bounds = _get_bounds(_raw_bounds)
|
|
438
|
+
if clickable:
|
|
439
|
+
covered_widget_ids = list(self.idx.contains(bounds))
|
|
440
|
+
if covered_widget_ids:
|
|
441
|
+
for covered_widget_id in covered_widget_ids:
|
|
442
|
+
node = self._nodes[covered_widget_id]
|
|
443
|
+
node.set("covered", "true")
|
|
444
|
+
self.idx.delete(
|
|
445
|
+
covered_widget_id,
|
|
446
|
+
_get_bounds(self._nodes[covered_widget_id].get("bounds"))
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
cur_id = len(self._nodes)
|
|
450
|
+
center = [
|
|
451
|
+
(bounds[0] + bounds[2]) / 2,
|
|
452
|
+
(bounds[1] + bounds[3]) / 2
|
|
453
|
+
]
|
|
454
|
+
self.idx.insert(
|
|
455
|
+
cur_id,
|
|
456
|
+
(center[0], center[1], center[0], center[1])
|
|
457
|
+
)
|
|
458
|
+
self._nodes.append(e)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class U2StaticDevice(u2.Device):
|
|
462
|
+
|
|
463
|
+
def __init__(self, script_driver=None):
|
|
464
|
+
self.xml: etree._Element = None
|
|
465
|
+
self._script_driver:u2.Device = script_driver
|
|
466
|
+
self._app_current = None
|
|
467
|
+
|
|
468
|
+
def __call__(self, **kwargs):
|
|
469
|
+
ui = StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
|
|
470
|
+
if self._script_driver:
|
|
471
|
+
ui.jsonrpc = self._script_driver.jsonrpc
|
|
472
|
+
return ui
|
|
473
|
+
|
|
474
|
+
def clear_cache(self):
|
|
475
|
+
self._app_current = None
|
|
476
|
+
|
|
477
|
+
def app_current(self):
|
|
478
|
+
if not self._app_current:
|
|
479
|
+
self._app_current = self._script_driver.app_current()
|
|
480
|
+
return self._app_current
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def xpath(self) -> u2.xpath.XPathEntry:
|
|
484
|
+
def get_page_source(self):
|
|
485
|
+
# print("[Debug] Using static get_page_source method")
|
|
486
|
+
xml_raw = etree.tostring(self._d.xml, encoding='unicode')
|
|
487
|
+
return u2.xpath.PageSource.parse(xml_raw)
|
|
488
|
+
xpathEntry = _XPathEntry(self)
|
|
489
|
+
xpathEntry.get_page_source = types.MethodType(
|
|
490
|
+
get_page_source, xpathEntry
|
|
491
|
+
)
|
|
492
|
+
return xpathEntry
|
|
493
|
+
|
|
494
|
+
def __getattr__(self, attr):
|
|
495
|
+
"""Proxy other methods to script_driver"""
|
|
496
|
+
logger.debug(f"{attr} not exists in static checker, proxy to script_driver.")
|
|
497
|
+
return getattr(self._script_driver, attr)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class _XPathEntry(u2.xpath.XPathEntry):
|
|
501
|
+
def __init__(self, d):
|
|
502
|
+
self.xpath = None
|
|
503
|
+
super().__init__(d)
|
|
504
|
+
|
|
505
|
+
# def __call__(self, xpath, source = None):
|
|
506
|
+
# TODO fully support xpath in widget.block.py
|
|
507
|
+
# self.xpath = xpath
|
|
508
|
+
# return super().__call__(xpath, source)
|
|
509
|
+
|
|
510
|
+
def __call__(self, xpath, source=None):
|
|
511
|
+
ui = StaticXpathUiObjectFactory.create(session=self, xpath=xpath, source=source)
|
|
512
|
+
return ui
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class U2StaticChecker(AbstractStaticChecker):
|
|
516
|
+
"""
|
|
517
|
+
This is the StaticChecker used to check the precondition.
|
|
518
|
+
We use the static checker due to the performing issues when runing multi-properties.
|
|
519
|
+
|
|
520
|
+
*e.g. the following self.d use U2StaticChecker*
|
|
521
|
+
```
|
|
522
|
+
@precondition(lambda self: self.d("battery").exists)
|
|
523
|
+
def test_battery(self):
|
|
524
|
+
...
|
|
525
|
+
```
|
|
526
|
+
"""
|
|
527
|
+
def __init__(self):
|
|
528
|
+
self.d = U2StaticDevice(U2ScriptDriver().getInstance())
|
|
529
|
+
|
|
530
|
+
def setHierarchy(self, hierarchy: str):
|
|
531
|
+
if hierarchy is None:
|
|
532
|
+
return
|
|
533
|
+
if isinstance(hierarchy, str):
|
|
534
|
+
self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
|
|
535
|
+
elif isinstance(hierarchy, etree._Element):
|
|
536
|
+
self.d.xml = hierarchy
|
|
537
|
+
elif isinstance(hierarchy, etree._ElementTree):
|
|
538
|
+
self.d.xml = hierarchy.getroot()
|
|
539
|
+
_HindenWidgetFilter(self.d.xml)
|
|
540
|
+
|
|
541
|
+
def getInstance(self, hierarchy: str=None):
|
|
542
|
+
self.setHierarchy(hierarchy)
|
|
543
|
+
return self.d
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
"""
|
|
547
|
+
The definition of U2Driver
|
|
548
|
+
"""
|
|
549
|
+
class U2Driver(AbstractDriver):
|
|
550
|
+
scriptDriver = None
|
|
551
|
+
staticChecker = None
|
|
552
|
+
|
|
553
|
+
@classmethod
|
|
554
|
+
def setDevice(cls, kwarg):
|
|
555
|
+
if kwarg.get("serial"):
|
|
556
|
+
U2ScriptDriver.setDeviceSerial(kwarg["serial"])
|
|
557
|
+
if kwarg.get("transport_id"):
|
|
558
|
+
U2ScriptDriver.setTransportId(kwarg["transport_id"])
|
|
559
|
+
|
|
560
|
+
@classmethod
|
|
561
|
+
def getScriptDriver(cls, mode:Literal["direct", "proxy"]="proxy") -> u2.Device:
|
|
562
|
+
"""
|
|
563
|
+
get the uiautomator2 device instance
|
|
564
|
+
mode: direct or proxy
|
|
565
|
+
direct: connect to device directly (device server port: 9008)
|
|
566
|
+
proxy: connect to device via kea2 agent (device server port: 8090)
|
|
567
|
+
"""
|
|
568
|
+
if cls.scriptDriver is None:
|
|
569
|
+
cls.scriptDriver = U2ScriptDriver()
|
|
570
|
+
_instance = cls.scriptDriver.getInstance()
|
|
571
|
+
_instance._device_server_port = 9008 if mode == "direct" else 8090
|
|
572
|
+
return _instance
|
|
573
|
+
|
|
574
|
+
@classmethod
|
|
575
|
+
def getStaticChecker(self, hierarchy=None):
|
|
576
|
+
if self.staticChecker is None:
|
|
577
|
+
self.staticChecker = U2StaticChecker()
|
|
578
|
+
return self.staticChecker.getInstance(hierarchy)
|
|
579
|
+
|
|
580
|
+
@classmethod
|
|
581
|
+
def tearDown(self):
|
|
582
|
+
if self.scriptDriver:
|
|
583
|
+
try:
|
|
584
|
+
self.scriptDriver.tearDown()
|
|
585
|
+
except Exception as e:
|
|
586
|
+
logger.debug(f"Error during U2Driver teardown: {e}")
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
"""
|
|
590
|
+
Other Utils
|
|
591
|
+
"""
|
|
592
|
+
def set_covered_to_deepest_node(selector: u2.Selector):
|
|
593
|
+
|
|
594
|
+
def find_deepest_nodes(node):
|
|
595
|
+
deepest_node = None
|
|
596
|
+
is_leaf = True
|
|
597
|
+
if "childOrSibling" in node and node["childOrSibling"]:
|
|
598
|
+
for i, relation in enumerate(node["childOrSibling"]):
|
|
599
|
+
sub_selector = node["childOrSiblingSelector"][i]
|
|
600
|
+
deepest_node = find_deepest_nodes(sub_selector)
|
|
601
|
+
is_leaf = False
|
|
602
|
+
|
|
603
|
+
if is_leaf:
|
|
604
|
+
deepest_node = node
|
|
605
|
+
return deepest_node
|
|
606
|
+
|
|
607
|
+
deepest_node = find_deepest_nodes(selector)
|
|
608
|
+
|
|
609
|
+
if deepest_node is not None:
|
|
610
|
+
dict.update(deepest_node, {"covered": False})
|