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/__init__.py +4 -0
- kea2/absDriver.py +34 -0
- kea2/adbUtils.py +258 -0
- kea2/assets/fastbot-thirdpart.jar +0 -0
- kea2/assets/fastbot_configs/ADBKeyBoard.apk +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/widget.block.py +22 -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/monkeyq.jar +0 -0
- kea2/assets/quickstart.py +90 -0
- kea2/assets/u2.jar +0 -0
- kea2/cli.py +176 -0
- kea2/keaUtils.py +535 -0
- kea2/kea_launcher.py +135 -0
- kea2/logWatcher.py +71 -0
- kea2/u2Driver.py +316 -0
- kea2/utils.py +53 -0
- kea2_python-0.0.1a0.dist-info/METADATA +433 -0
- kea2_python-0.0.1a0.dist-info/RECORD +33 -0
- kea2_python-0.0.1a0.dist-info/WHEEL +5 -0
- kea2_python-0.0.1a0.dist-info/entry_points.txt +2 -0
- kea2_python-0.0.1a0.dist-info/licenses/LICENSE +16 -0
- kea2_python-0.0.1a0.dist-info/top_level.txt +1 -0
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
|