Kea2-python 0.0.1a0__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/logWatcher.py ADDED
@@ -0,0 +1,71 @@
1
+ import re
2
+ import os
3
+ import threading
4
+ import time
5
+
6
+
7
+ PATTERN_EXCEPTION = re.compile(r"\[Fastbot\].+Internal\serror\n([\s\S]*)")
8
+ PATTERN_STATISTIC = re.compile(r".+Monkey\sis\sover!\n([\s\S]+)")
9
+
10
+
11
+ def thread_excepthook(args):
12
+ print(args.exc_value, flush=True)
13
+ os._exit(1)
14
+
15
+
16
+
17
+ class LogWatcher:
18
+
19
+ def watcher(self, poll_interval=1):
20
+ self.buffer = ""
21
+ self.last_pos = 0
22
+
23
+ while True:
24
+ self.read_log()
25
+ time.sleep(poll_interval)
26
+
27
+ def read_log(self):
28
+ time.sleep(0.02)
29
+ with open(self.log_file, 'r', encoding='utf-8') as f:
30
+ f.seek(self.last_pos)
31
+ new_data = f.read()
32
+ self.last_pos = f.tell()
33
+
34
+ if new_data:
35
+ self.buffer += new_data
36
+ self.parse_log()
37
+
38
+ def parse_log(self):
39
+ buffer = self.buffer
40
+ exception_match = PATTERN_EXCEPTION.search(buffer)
41
+ if exception_match:
42
+ exception_body = exception_match.group(1).strip()
43
+ if exception_body:
44
+ raise RuntimeError(
45
+ "[Error] Execption while running fastbot:\n" +
46
+ exception_body +
47
+ "\nSee fastbot.log for details."
48
+ )
49
+ statistic_match = PATTERN_STATISTIC.search(buffer)
50
+ if statistic_match:
51
+ statistic_body = statistic_match.group(1).strip()
52
+ if statistic_body:
53
+ print(
54
+ "[INFO] Fastbot exit:\n" +
55
+ statistic_body
56
+ , flush=True)
57
+
58
+ def __init__(self, log_file):
59
+ self.log_file = log_file
60
+
61
+ threading.excepthook = thread_excepthook
62
+ t = threading.Thread(target=self.watcher, daemon=True)
63
+ t.start()
64
+
65
+ def close(self):
66
+ time.sleep(0.2) # wait for the written logfile close
67
+ self.read_log()
68
+
69
+
70
+ if __name__ == "__main__":
71
+ LogWatcher("fastbot.log")
kea2/u2Driver.py ADDED
@@ -0,0 +1,316 @@
1
+ import random
2
+ import socket
3
+ import uiautomator2 as u2
4
+ import types
5
+ import rtree
6
+ import re
7
+ from typing import Dict, List, Union
8
+ from lxml import etree
9
+ from .absDriver import AbstractScriptDriver, AbstractStaticChecker, AbstractDriver
10
+ from .adbUtils import list_forwards, remove_forward, create_forward
11
+ from .utils import TimeStamp
12
+
13
+ TIME_STAMP = TimeStamp().getTimeStamp()
14
+
15
+ """
16
+ The definition of U2ScriptDriver
17
+ """
18
+ class U2ScriptDriver(AbstractScriptDriver):
19
+ """
20
+ This is the ScriptDriver used to send ui automation request in Property
21
+ When you interact with the mobile in properties. You will use the object here
22
+
23
+ *e.g. the following self.d use U2ScriptDriver*
24
+ ```
25
+ @precondition(...)
26
+ def test_battery(self):
27
+ self.d(text="battery").click()
28
+ ```
29
+ """
30
+
31
+ deviceSerial: str = None
32
+
33
+ @classmethod
34
+ def setDeviceSerial(cls, deviceSerial):
35
+ cls.deviceSerial = deviceSerial
36
+
37
+ def __init__(self):
38
+ self.d = None
39
+
40
+ def getInstance(self):
41
+ if self.d is None:
42
+ self.d = (
43
+ u2.connect() if self.deviceSerial is None
44
+ else u2.connect(self.deviceSerial)
45
+ )
46
+
47
+ def get_u2_forward_port() -> int:
48
+ """rewrite forward_port mothod to avoid the relocation of port
49
+ :return: the new forward port
50
+ """
51
+ print("Rewriting forward_port method", flush=True)
52
+ self.d._dev.forward_port = types.MethodType(
53
+ forward_port, self.d._dev)
54
+ lport = self.d._dev.forward_port(8090)
55
+ setattr(self.d._dev, "msg", "meta")
56
+ print(f"[U2] local port: {lport}", flush=True)
57
+ return lport
58
+
59
+ self._remove_remote_port(8090)
60
+ self.d.lport = get_u2_forward_port()
61
+ self._remove_remote_port(9008)
62
+
63
+ return self.d
64
+
65
+ def tearDown(self):
66
+ self.d.stop_uiautomator()
67
+
68
+ def _remove_remote_port(self, port:int):
69
+ """remove the forward port
70
+ """
71
+ forwardLists = list_forwards(device=self.deviceSerial)
72
+ for forward in forwardLists:
73
+ if forward["remote"] == f"tcp:{port}":
74
+ forward_local = forward["local"]
75
+ remove_forward(local_spec=forward_local, device=self.deviceSerial)
76
+
77
+
78
+ """
79
+ The definition of U2StaticChecker
80
+ """
81
+ class StaticU2UiObject(u2.UiObject):
82
+ def __init__(self, session, selector):
83
+ self.session: U2StaticDevice = session
84
+ self.selector = selector
85
+
86
+ def _transferU2Keys(self, originKey):
87
+ filterDict = {
88
+ "resourceId": "resource-id"
89
+ }
90
+ if filterDict.get(originKey, None):
91
+ return filterDict[originKey]
92
+ return originKey
93
+
94
+ def _getXPath(self, kwargs: Dict[str, str]):
95
+
96
+ def filter_selectors(kwargs: Dict[str, str]):
97
+ """
98
+ filter the selector
99
+ """
100
+ new_kwargs = dict()
101
+ SPECIAL_KEY = {"mask", "childOrSibling", "childOrSiblingSelector"}
102
+ for key, val in kwargs.items():
103
+ if key in SPECIAL_KEY:
104
+ continue
105
+ key = self._transferU2Keys(key)
106
+ new_kwargs[key] = val
107
+ return new_kwargs
108
+
109
+ kwargs = filter_selectors(kwargs)
110
+
111
+ attrLocs = [
112
+ f"[@{k}='{v}']" for k, v in kwargs.items()
113
+ ]
114
+ xpath = f".//node{''.join(attrLocs)}"
115
+ return xpath
116
+
117
+ @property
118
+ def exists(self):
119
+ self.selector["covered"] = "true"
120
+ xpath = self._getXPath(self.selector)
121
+ matched_widgets = self.session.xml.xpath(xpath)
122
+ return bool(matched_widgets)
123
+
124
+ def __len__(self):
125
+ xpath = self._getXPath(self.selector)
126
+ matched_widgets = self.session.xml.xpath(xpath)
127
+ return len(matched_widgets)
128
+
129
+
130
+ def _get_bounds(raw_bounds):
131
+ pattern = re.compile(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]")
132
+ m = re.match(pattern, raw_bounds)
133
+ try:
134
+ bounds = [int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))]
135
+ except Exception as e:
136
+ print(f"raw_bounds: {raw_bounds}", flush=True)
137
+ print(f"Please report this bug to Kea2", flush=True)
138
+ raise RuntimeError(e)
139
+
140
+ return bounds
141
+
142
+
143
+ class _HindenWidgetFilter:
144
+ def __init__(self, root: etree._Element):
145
+ # self.global_drawing_order = 0
146
+ self._nodes = []
147
+
148
+ self.idx = rtree.index.Index()
149
+ self.set_covered_attr(root)
150
+
151
+ # xml_bytes = etree.tostring(root, pretty_print=True, encoding="utf-8", xml_declaration=True)
152
+ # with open("filtered_tree.xml", "wb") as f:
153
+ # f.write(xml_bytes)
154
+ # xml_bytes
155
+
156
+ def _iter_by_drawing_order(self, ele: etree._Element):
157
+ """
158
+ iter by drawing order (DFS)
159
+ """
160
+ if ele.tag == "node":
161
+ yield ele
162
+
163
+ children = list(ele)
164
+ try:
165
+ children.sort(key=lambda e: int(e.get("drawing-order", 0)))
166
+ except (TypeError, ValueError):
167
+ pass
168
+
169
+ for child in children:
170
+ yield from self._iter_by_drawing_order(child)
171
+
172
+ def set_covered_attr(self, root: etree._Element):
173
+ self._nodes: List[etree._Element] = list()
174
+ for e in self._iter_by_drawing_order(root):
175
+ # e.set("global-order", str(self.global_drawing_order))
176
+ # self.global_drawing_order += 1
177
+ e.set("covered", "false")
178
+
179
+ # algorithm: filter by "clickable"
180
+ clickable = (e.get("clickable", "false") == "true")
181
+ _raw_bounds = e.get("bounds")
182
+ if _raw_bounds is None:
183
+ continue
184
+ bounds = _get_bounds(_raw_bounds)
185
+ if clickable:
186
+ covered_widget_ids = list(self.idx.contains(bounds))
187
+ if covered_widget_ids:
188
+ for covered_widget_id in covered_widget_ids:
189
+ node = self._nodes[covered_widget_id]
190
+ node.set("covered", "true")
191
+ self.idx.delete(
192
+ covered_widget_id,
193
+ _get_bounds(self._nodes[covered_widget_id].get("bounds"))
194
+ )
195
+
196
+ cur_id = len(self._nodes)
197
+ center = [
198
+ (bounds[0] + bounds[2]) / 2,
199
+ (bounds[1] + bounds[3]) / 2
200
+ ]
201
+ self.idx.insert(
202
+ cur_id,
203
+ (center[0], center[1], center[0], center[1])
204
+ )
205
+ self._nodes.append(e)
206
+
207
+ class U2StaticDevice(u2.Device):
208
+ def __init__(self):
209
+ self.xml: etree._Element = None
210
+
211
+ def __call__(self, **kwargs):
212
+ return StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
213
+
214
+ @property
215
+ def xpath(self) -> u2.xpath.XPathEntry:
216
+ def get_page_source(self):
217
+ # print("[Debug] Using static get_page_source method")
218
+ return u2.xpath.PageSource.parse(self._d.xml_raw)
219
+ xpathEntry = u2.xpath.XPathEntry(self)
220
+ xpathEntry.get_page_source = types.MethodType(
221
+ get_page_source, xpathEntry
222
+ )
223
+ return xpathEntry
224
+
225
+
226
+ class U2StaticChecker(AbstractStaticChecker):
227
+ """
228
+ This is the StaticChecker used to check the precondition.
229
+ We use the static checker due to the performing issues when runing multi-properties.
230
+
231
+ *e.g. the following self.d use U2StaticChecker*
232
+ ```
233
+ @precondition(lambda self: self.d("battery").exists)
234
+ def test_battery(self):
235
+ ...
236
+ ```
237
+ """
238
+ def __init__(self):
239
+ self.d = U2StaticDevice()
240
+
241
+ def setHierarchy(self, hierarchy: str):
242
+ if hierarchy is None:
243
+ return
244
+ self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
245
+ _HindenWidgetFilter(self.d.xml)
246
+
247
+ def getInstance(self, hierarchy: str=None):
248
+ self.setHierarchy(hierarchy)
249
+ return self.d
250
+
251
+
252
+ """
253
+ The definition of U2Driver
254
+ """
255
+ class U2Driver(AbstractDriver):
256
+ scriptDriver = None
257
+ staticChecker = None
258
+
259
+ @classmethod
260
+ def setDeviceSerial(cls, deviceSerial):
261
+ U2ScriptDriver.setDeviceSerial(deviceSerial)
262
+
263
+ @classmethod
264
+ def getScriptDriver(self):
265
+ if self.scriptDriver is None:
266
+ self.scriptDriver = U2ScriptDriver()
267
+ return self.scriptDriver.getInstance()
268
+
269
+ @classmethod
270
+ def getStaticChecker(self, hierarchy=None):
271
+ if self.staticChecker is None:
272
+ self.staticChecker = U2StaticChecker()
273
+ return self.staticChecker.getInstance(hierarchy)
274
+
275
+
276
+ """
277
+ Other Utils
278
+ """
279
+ def forward_port(self, remote: Union[int, str]) -> int:
280
+ """forward remote port to local random port"""
281
+ remote = 8090
282
+ if isinstance(remote, int):
283
+ remote = "tcp:" + str(remote)
284
+ for f in self.forward_list():
285
+ if (
286
+ f.serial == self._serial
287
+ and f.remote == remote
288
+ and f.local.startswith("tcp:")
289
+ ): # yapf: disable
290
+ return int(f.local[len("tcp:") :])
291
+ local_port = get_free_port()
292
+ self.forward("tcp:" + str(local_port), remote)
293
+ return local_port
294
+
295
+
296
+ def is_port_in_use(port: int) -> bool:
297
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
298
+ return s.connect_ex(('127.0.0.1', port)) == 0
299
+
300
+
301
+ def get_free_port():
302
+ try:
303
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
304
+ s.bind(('127.0.0.1', 0))
305
+ try:
306
+ return s.getsockname()[1]
307
+ finally:
308
+ s.close()
309
+ except OSError:
310
+ # bind 0 will fail on Manjaro, fallback to random port
311
+ # https://github.com/openatx/adbutils/issues/85
312
+ for _ in range(20):
313
+ port = random.randint(10000, 20000)
314
+ if not is_port_in_use(port):
315
+ return port
316
+ raise RuntimeError("No free port found")
kea2/utils.py ADDED
@@ -0,0 +1,53 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+
5
+
6
+ def getLogger(name: str) -> logging.Logger:
7
+ logger = logging.getLogger(name)
8
+
9
+ def enable_pretty_logging():
10
+ if not logger.handlers:
11
+ # Configure handler
12
+ handler = logging.StreamHandler()
13
+ formatter = logging.Formatter('[%(levelname)1s][%(asctime)s %(module)s:%(lineno)d pid:%(process)d] %(message)s')
14
+ handler.setFormatter(formatter)
15
+ logger.addHandler(handler)
16
+ logger.propagate = False
17
+
18
+ enable_pretty_logging()
19
+ return logger
20
+
21
+
22
+ def singleton(cls):
23
+ _instance = {}
24
+
25
+ def inner():
26
+ if cls not in _instance:
27
+ _instance[cls] = cls()
28
+ return _instance[cls]
29
+ return inner
30
+
31
+ @singleton
32
+ class TimeStamp:
33
+ time_stamp = None
34
+
35
+ def getTimeStamp(cls):
36
+ if cls.time_stamp is None:
37
+ import datetime
38
+ cls.time_stamp = datetime.datetime.now().strftime('%Y%m%d%H_%M%S%f')
39
+ return cls.time_stamp
40
+
41
+
42
+ from uiautomator2 import Device
43
+ d = Device
44
+
45
+
46
+ def getProjectRoot():
47
+ root = Path("/")
48
+ cur_dir = Path.absolute(Path(os.curdir))
49
+ while not os.path.isdir(cur_dir / "configs"):
50
+ if cur_dir == root:
51
+ return None
52
+ cur_dir = cur_dir.parent
53
+ return cur_dir