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.

Files changed (52) hide show
  1. kea2/__init__.py +3 -0
  2. kea2/absDriver.py +56 -0
  3. kea2/adbUtils.py +554 -0
  4. kea2/assets/config_version.json +16 -0
  5. kea2/assets/fastbot-thirdpart.jar +0 -0
  6. kea2/assets/fastbot_configs/abl.strings +2 -0
  7. kea2/assets/fastbot_configs/awl.strings +3 -0
  8. kea2/assets/fastbot_configs/max.config +7 -0
  9. kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
  10. kea2/assets/fastbot_configs/max.schema.strings +1 -0
  11. kea2/assets/fastbot_configs/max.strings +3 -0
  12. kea2/assets/fastbot_configs/max.tree.pruning +27 -0
  13. kea2/assets/fastbot_configs/teardown.py +18 -0
  14. kea2/assets/fastbot_configs/widget.block.py +38 -0
  15. kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  16. kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  17. kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  18. kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  19. kea2/assets/framework.jar +0 -0
  20. kea2/assets/kea2-thirdpart.jar +0 -0
  21. kea2/assets/monkeyq.jar +0 -0
  22. kea2/assets/quicktest.py +126 -0
  23. kea2/cli.py +320 -0
  24. kea2/fastbotManager.py +267 -0
  25. kea2/fastbotx/ActivityTimes.py +52 -0
  26. kea2/fastbotx/ReuseEntry.py +74 -0
  27. kea2/fastbotx/ReuseModel.py +63 -0
  28. kea2/fastbotx/__init__.py +7 -0
  29. kea2/fbm_parser.py +871 -0
  30. kea2/fs_lock.py +131 -0
  31. kea2/kea2_api.py +166 -0
  32. kea2/keaUtils.py +1112 -0
  33. kea2/kea_launcher.py +319 -0
  34. kea2/logWatcher.py +92 -0
  35. kea2/mixin.py +22 -0
  36. kea2/report/__init__.py +0 -0
  37. kea2/report/bug_report_generator.py +793 -0
  38. kea2/report/mixin.py +482 -0
  39. kea2/report/report_merger.py +797 -0
  40. kea2/report/templates/bug_report_template.html +3876 -0
  41. kea2/report/templates/merged_bug_report_template.html +3333 -0
  42. kea2/report/utils.py +10 -0
  43. kea2/resultSyncer.py +65 -0
  44. kea2/u2Driver.py +610 -0
  45. kea2/utils.py +184 -0
  46. kea2/version_manager.py +102 -0
  47. kea2_python-1.0.6b0.dist-info/METADATA +447 -0
  48. kea2_python-1.0.6b0.dist-info/RECORD +52 -0
  49. kea2_python-1.0.6b0.dist-info/WHEEL +5 -0
  50. kea2_python-1.0.6b0.dist-info/entry_points.txt +2 -0
  51. kea2_python-1.0.6b0.dist-info/licenses/LICENSE +16 -0
  52. 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})