Kea2-python 0.1.2__py3-none-any.whl → 0.2.0__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/absDriver.py +1 -1
- kea2/adbUtils.py +273 -20
- kea2/bug_report_generator.py +333 -321
- kea2/fastbotManager.py +105 -47
- kea2/keaUtils.py +100 -95
- kea2/kea_launcher.py +15 -0
- kea2/resultSyncer.py +18 -8
- kea2/templates/bug_report_template.html +9 -50
- kea2/u2Driver.py +148 -137
- kea2/utils.py +1 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/METADATA +44 -33
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/RECORD +16 -16
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/WHEEL +0 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.1.2.dist-info → kea2_python-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -356,48 +356,7 @@
|
|
|
356
356
|
|
|
357
357
|
<!-- Key Statistics -->
|
|
358
358
|
<div class="row g-4 mb-4">
|
|
359
|
-
<div class="col-
|
|
360
|
-
<div class="stats-card">
|
|
361
|
-
<div class="card-header bg-primary text-white">
|
|
362
|
-
<i class="bi bi-stopwatch"></i> Time Statistics
|
|
363
|
-
</div>
|
|
364
|
-
<div class="card-body">
|
|
365
|
-
<div class="mb-3">
|
|
366
|
-
<h5 class="d-flex justify-content-between">
|
|
367
|
-
<span>First Bug Discovery Time:</span>
|
|
368
|
-
<span class="value-danger">{{ (first_bug_time / 60)|int }} min: {{ (first_bug_time % 60)|int }} sec</span>
|
|
369
|
-
</h5>
|
|
370
|
-
<div class="progress">
|
|
371
|
-
<div class="progress-bar bg-danger" role="progressbar"
|
|
372
|
-
style="width: calc({{ first_bug_time }} / {{ total_testing_time }} * 100%);"
|
|
373
|
-
aria-valuenow="{{ first_bug_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
<div class="mb-3">
|
|
377
|
-
<h5 class="d-flex justify-content-between">
|
|
378
|
-
<span>First Precondition Satisfaction Time:</span>
|
|
379
|
-
<span class="value-success">{{ (first_precondition_time / 60)|int }} min: {{ (first_precondition_time % 60)|int }} sec</span>
|
|
380
|
-
</h5>
|
|
381
|
-
<div class="progress">
|
|
382
|
-
<div class="progress-bar bg-success" role="progressbar"
|
|
383
|
-
style="width: calc({{ first_precondition_time }} / {{ total_testing_time }} * 100%);"
|
|
384
|
-
aria-valuenow="{{ first_precondition_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
|
|
385
|
-
</div>
|
|
386
|
-
</div>
|
|
387
|
-
<div>
|
|
388
|
-
<h5 class="d-flex justify-content-between">
|
|
389
|
-
<span>Total Testing Time:</span>
|
|
390
|
-
<span class="value-primary">{{ (total_testing_time / 60)|int }} min: {{ (total_testing_time % 60)|int }} sec</span>
|
|
391
|
-
</h5>
|
|
392
|
-
<div class="progress">
|
|
393
|
-
<div class="progress-bar bg-primary" role="progressbar" style="width: 100%;"
|
|
394
|
-
aria-valuenow="{{ total_testing_time }}" aria-valuemin="0" aria-valuemax="{{ total_testing_time }}"></div>
|
|
395
|
-
</div>
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
399
|
-
</div>
|
|
400
|
-
<div class="col-md-6">
|
|
359
|
+
<div class="col-12">
|
|
401
360
|
<div class="stats-card">
|
|
402
361
|
<div class="card-header bg-success text-white">
|
|
403
362
|
<i class="bi bi-bar-chart"></i> Coverage Statistics
|
|
@@ -437,7 +396,7 @@
|
|
|
437
396
|
<!-- Tested Activities Panel -->
|
|
438
397
|
<div class="col-md-6">
|
|
439
398
|
<div class="card">
|
|
440
|
-
<div class="card-header bg-
|
|
399
|
+
<div class="card-header bg-warning text-white">
|
|
441
400
|
<div class="d-flex justify-content-between align-items-center">
|
|
442
401
|
<span><i class="bi bi-check-circle"></i> Tested Activities ({{ tested_activities|length }})</span>
|
|
443
402
|
<span class="badge bg-light text-dark">{{ tested_activities|length }} / {{ total_activities_count }}</span>
|
|
@@ -605,14 +564,14 @@
|
|
|
605
564
|
</tr>
|
|
606
565
|
</thead>
|
|
607
566
|
<tbody id="property-stats-container">
|
|
608
|
-
{% for
|
|
567
|
+
{% for property_name, test_result in property_stats.items() %}
|
|
609
568
|
<tr class="property-stat-row" data-page="1">
|
|
610
|
-
<td>{{
|
|
611
|
-
<td><span class="badge bg-light text-dark badge-custom">{{
|
|
612
|
-
<td>{{
|
|
613
|
-
<td>{{
|
|
614
|
-
<td><span class="badge bg-danger text-white">{{
|
|
615
|
-
<td><span class="badge bg-warning text-dark">{{
|
|
569
|
+
<td>{{ loop.index }}</td>
|
|
570
|
+
<td><span class="badge bg-light text-dark badge-custom">{{ property_name }}</span></td>
|
|
571
|
+
<td>{{ test_result.precond_satisfied|default(0) }}</td>
|
|
572
|
+
<td>{{ test_result.executed|default(0) }}</td>
|
|
573
|
+
<td><span class="badge bg-danger text-white">{{ test_result.fail|default(0) }}</span></td>
|
|
574
|
+
<td><span class="badge bg-warning text-dark">{{ test_result.error|default(0) }}</span></td>
|
|
616
575
|
</tr>
|
|
617
576
|
{% endfor %}
|
|
618
577
|
</tbody>
|
kea2/u2Driver.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import random
|
|
2
2
|
import socket
|
|
3
|
+
from time import sleep
|
|
3
4
|
import uiautomator2 as u2
|
|
5
|
+
import adbutils
|
|
4
6
|
import types
|
|
5
7
|
import rtree
|
|
6
8
|
import re
|
|
7
|
-
from typing import Dict, List, Union
|
|
9
|
+
from typing import Any, Dict, List, Union
|
|
10
|
+
from http.client import HTTPResponse
|
|
8
11
|
from lxml import etree
|
|
9
12
|
from .absDriver import AbstractScriptDriver, AbstractStaticChecker, AbstractDriver
|
|
10
13
|
from .adbUtils import list_forwards, remove_forward, create_forward
|
|
@@ -16,7 +19,7 @@ import logging
|
|
|
16
19
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
|
17
20
|
logging.getLogger("uiautomator2").setLevel(logging.INFO)
|
|
18
21
|
|
|
19
|
-
logger = getLogger(__name__)
|
|
22
|
+
logger = getLogger(__name__)
|
|
20
23
|
|
|
21
24
|
"""
|
|
22
25
|
The definition of U2ScriptDriver
|
|
@@ -35,34 +38,24 @@ class U2ScriptDriver(AbstractScriptDriver):
|
|
|
35
38
|
"""
|
|
36
39
|
|
|
37
40
|
deviceSerial: str = None
|
|
41
|
+
transportId: str = None
|
|
38
42
|
d = None
|
|
39
43
|
|
|
44
|
+
@classmethod
|
|
45
|
+
def setTransportId(cls, transportId):
|
|
46
|
+
cls.transportId = transportId
|
|
47
|
+
|
|
40
48
|
@classmethod
|
|
41
49
|
def setDeviceSerial(cls, deviceSerial):
|
|
42
50
|
cls.deviceSerial = deviceSerial
|
|
43
51
|
|
|
44
52
|
def getInstance(self):
|
|
45
53
|
if self.d is None:
|
|
46
|
-
self.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
def get_u2_forward_port() -> int:
|
|
52
|
-
"""rewrite forward_port mothod to avoid the relocation of port
|
|
53
|
-
:return: the new forward port
|
|
54
|
-
"""
|
|
55
|
-
print("Rewriting forward_port method", flush=True)
|
|
56
|
-
self.d._dev.forward_port = types.MethodType(
|
|
57
|
-
forward_port, self.d._dev)
|
|
58
|
-
lport = self.d._dev.forward_port(8090)
|
|
59
|
-
setattr(self.d._dev, "msg", "meta")
|
|
60
|
-
print(f"[U2] local port: {lport}", flush=True)
|
|
61
|
-
return lport
|
|
62
|
-
|
|
63
|
-
self._remove_remote_port(8090)
|
|
64
|
-
self.d.lport = get_u2_forward_port()
|
|
65
|
-
self._remove_remote_port(9008)
|
|
54
|
+
adb = adbutils.device(serial=self.deviceSerial, transport_id=self.transportId)
|
|
55
|
+
print("[INFO] Connecting to uiautomator2. Please wait ...")
|
|
56
|
+
self.d = u2.connect(adb)
|
|
57
|
+
sleep(5)
|
|
58
|
+
self.d._device_server_port = 8090
|
|
66
59
|
|
|
67
60
|
return self.d
|
|
68
61
|
|
|
@@ -76,10 +69,11 @@ class U2ScriptDriver(AbstractScriptDriver):
|
|
|
76
69
|
remove_forward(local_spec=forward_local, device=self.deviceSerial)
|
|
77
70
|
|
|
78
71
|
def tearDown(self):
|
|
79
|
-
logger.debug("U2Driver tearDown: stop_uiautomator")
|
|
80
|
-
self.d.stop_uiautomator()
|
|
81
|
-
logger.debug("U2Driver tearDown: remove forward")
|
|
82
|
-
self._remove_remote_port(8090)
|
|
72
|
+
# logger.debug("U2Driver tearDown: stop_uiautomator")
|
|
73
|
+
# self.d.stop_uiautomator()
|
|
74
|
+
# logger.debug("U2Driver tearDown: remove forward")
|
|
75
|
+
# self._remove_remote_port(8090)
|
|
76
|
+
pass
|
|
83
77
|
|
|
84
78
|
"""
|
|
85
79
|
The definition of U2StaticChecker
|
|
@@ -100,39 +94,105 @@ class StaticU2UiObject(u2.UiObject):
|
|
|
100
94
|
return filterDict[originKey]
|
|
101
95
|
return originKey
|
|
102
96
|
|
|
103
|
-
def
|
|
97
|
+
def selector_to_xpath(self, selector: u2.Selector, is_initial: bool = True) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"""
|
|
109
|
-
new_kwargs = dict()
|
|
110
|
-
SPECIAL_KEY = {"mask", "childOrSibling", "childOrSiblingSelector"}
|
|
111
|
-
for key, val in kwargs.items():
|
|
112
|
-
if key in SPECIAL_KEY:
|
|
113
|
-
continue
|
|
114
|
-
key = self._transferU2Keys(key)
|
|
115
|
-
new_kwargs[key] = val
|
|
116
|
-
return new_kwargs
|
|
101
|
+
Args:
|
|
102
|
+
selector (u2.Selector): A u2 Selector object
|
|
103
|
+
is_initial (bool): Whether it is the initial node, defaults to True
|
|
117
104
|
|
|
118
|
-
|
|
105
|
+
Returns:
|
|
106
|
+
str: The corresponding XPath expression
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
119
109
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
xpath = ".//node" if is_initial else "node"
|
|
111
|
+
|
|
112
|
+
conditions = []
|
|
113
|
+
|
|
114
|
+
if "className" in selector:
|
|
115
|
+
conditions.insert(0, f"[@class='{selector['className']}']")
|
|
116
|
+
|
|
117
|
+
if "text" in selector:
|
|
118
|
+
conditions.append(f"[@text='{selector['text']}']")
|
|
119
|
+
elif "textContains" in selector:
|
|
120
|
+
conditions.append(f"[contains(@text, '{selector['textContains']}')]")
|
|
121
|
+
elif "textStartsWith" in selector:
|
|
122
|
+
conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
|
|
123
|
+
elif "textMatches" in selector:
|
|
124
|
+
raise NotImplementedError("'textMatches' syntax is not supported")
|
|
125
|
+
|
|
126
|
+
if "description" in selector:
|
|
127
|
+
conditions.append(f"[@content-desc='{selector['description']}']")
|
|
128
|
+
elif "descriptionContains" in selector:
|
|
129
|
+
conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
|
|
130
|
+
elif "descriptionStartsWith" in selector:
|
|
131
|
+
conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
|
|
132
|
+
elif "descriptionMatches" in selector:
|
|
133
|
+
raise NotImplementedError("'descriptionMatches' syntax is not supported")
|
|
134
|
+
|
|
135
|
+
if "packageName" in selector:
|
|
136
|
+
conditions.append(f"[@package='{selector['packageName']}']")
|
|
137
|
+
elif "packageNameMatches" in selector:
|
|
138
|
+
raise NotImplementedError("'packageNameMatches' syntax is not supported")
|
|
139
|
+
|
|
140
|
+
if "resourceId" in selector:
|
|
141
|
+
conditions.append(f"[@resource-id='{selector['resourceId']}']")
|
|
142
|
+
elif "resourceIdMatches" in selector:
|
|
143
|
+
raise NotImplementedError("'resourceIdMatches' syntax is not supported")
|
|
144
|
+
|
|
145
|
+
bool_props = ["checkable", "checked", "clickable", "longClickable", "scrollable", "enabled", "focusable",
|
|
146
|
+
"focused", "selected", "covered"]
|
|
147
|
+
|
|
148
|
+
def str_to_bool(value):
|
|
149
|
+
"""Convert string 'true'/'false' to boolean, or return original value if already boolean"""
|
|
150
|
+
if isinstance(value, str):
|
|
151
|
+
return value.lower() == "true"
|
|
152
|
+
return bool(value)
|
|
153
|
+
|
|
154
|
+
for prop in bool_props:
|
|
155
|
+
if prop in selector:
|
|
156
|
+
bool_value = str_to_bool(selector[prop])
|
|
157
|
+
value = "true" if bool_value else "false"
|
|
158
|
+
conditions.append(f"[@{prop}='{value}']")
|
|
159
|
+
|
|
160
|
+
if "index" in selector:
|
|
161
|
+
conditions.append(f"[@index='{selector['index']}']")
|
|
162
|
+
|
|
163
|
+
xpath += "".join(conditions)
|
|
164
|
+
|
|
165
|
+
if "childOrSibling" in selector and selector["childOrSibling"]:
|
|
166
|
+
for i, relation in enumerate(selector["childOrSibling"]):
|
|
167
|
+
sub_selector = selector["childOrSiblingSelector"][i]
|
|
168
|
+
sub_xpath = self.selector_to_xpath(sub_selector, False)
|
|
169
|
+
|
|
170
|
+
if relation == "child":
|
|
171
|
+
xpath += f"//{sub_xpath}"
|
|
172
|
+
elif relation == "sibling":
|
|
173
|
+
cur_root = xpath
|
|
174
|
+
following_sibling = cur_root + f"/following-sibling::{sub_xpath}"
|
|
175
|
+
preceding_sibling = cur_root + f"/preceding-sibling::{sub_xpath}"
|
|
176
|
+
xpath = f"({following_sibling} | {preceding_sibling})"
|
|
177
|
+
if "instance" in selector:
|
|
178
|
+
xpath = f"({xpath})[{selector['instance'] + 1}]"
|
|
179
|
+
|
|
180
|
+
return xpath
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Error occurred during selector conversion: {e}")
|
|
184
|
+
return "//error"
|
|
125
185
|
|
|
126
186
|
|
|
127
187
|
@property
|
|
128
188
|
def exists(self):
|
|
129
|
-
|
|
130
|
-
xpath = self.
|
|
189
|
+
set_covered_to_deepest_node(self.selector)
|
|
190
|
+
xpath = self.selector_to_xpath(self.selector)
|
|
131
191
|
matched_widgets = self.session.xml.xpath(xpath)
|
|
132
192
|
return bool(matched_widgets)
|
|
133
193
|
|
|
134
194
|
def __len__(self):
|
|
135
|
-
xpath = self.
|
|
195
|
+
xpath = self.selector_to_xpath(self.selector)
|
|
136
196
|
matched_widgets = self.session.xml.xpath(xpath)
|
|
137
197
|
return len(matched_widgets)
|
|
138
198
|
|
|
@@ -142,6 +202,9 @@ class StaticU2UiObject(u2.UiObject):
|
|
|
142
202
|
def sibling(self, **kwargs):
|
|
143
203
|
return StaticU2UiObject(self.session, self.selector.clone().sibling(**kwargs))
|
|
144
204
|
|
|
205
|
+
def __getattr__(self, attr):
|
|
206
|
+
return getattr(super(), attr)
|
|
207
|
+
|
|
145
208
|
|
|
146
209
|
def _get_bounds(raw_bounds):
|
|
147
210
|
pattern = re.compile(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]")
|
|
@@ -227,7 +290,9 @@ class U2StaticDevice(u2.Device):
|
|
|
227
290
|
self._script_driver = script_driver
|
|
228
291
|
|
|
229
292
|
def __call__(self, **kwargs):
|
|
230
|
-
|
|
293
|
+
ui = StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
|
|
294
|
+
ui.jsonrpc = self._script_driver.jsonrpc
|
|
295
|
+
return ui
|
|
231
296
|
|
|
232
297
|
@property
|
|
233
298
|
def xpath(self) -> u2.xpath.XPathEntry:
|
|
@@ -274,7 +339,12 @@ class U2StaticChecker(AbstractStaticChecker):
|
|
|
274
339
|
def setHierarchy(self, hierarchy: str):
|
|
275
340
|
if hierarchy is None:
|
|
276
341
|
return
|
|
277
|
-
|
|
342
|
+
if isinstance(hierarchy, str):
|
|
343
|
+
self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
|
|
344
|
+
elif isinstance(hierarchy, etree._Element):
|
|
345
|
+
self.d.xml = hierarchy
|
|
346
|
+
elif isinstance(hierarchy, etree._ElementTree):
|
|
347
|
+
self.d.xml = hierarchy.getroot()
|
|
278
348
|
_HindenWidgetFilter(self.d.xml)
|
|
279
349
|
|
|
280
350
|
def getInstance(self, hierarchy: str=None):
|
|
@@ -290,8 +360,11 @@ class U2Driver(AbstractDriver):
|
|
|
290
360
|
staticChecker = None
|
|
291
361
|
|
|
292
362
|
@classmethod
|
|
293
|
-
def
|
|
294
|
-
|
|
363
|
+
def setDevice(cls, kwarg):
|
|
364
|
+
if kwarg.get("serial"):
|
|
365
|
+
U2ScriptDriver.setDeviceSerial(kwarg["serial"])
|
|
366
|
+
if kwarg.get("transport_id"):
|
|
367
|
+
U2ScriptDriver.setTransportId(kwarg["transport_id"])
|
|
295
368
|
|
|
296
369
|
@classmethod
|
|
297
370
|
def getScriptDriver(self):
|
|
@@ -324,96 +397,12 @@ def forward_port(self, remote: Union[int, str]) -> int:
|
|
|
324
397
|
and f.remote == remote
|
|
325
398
|
and f.local.startswith("tcp:")
|
|
326
399
|
): # yapf: disable
|
|
327
|
-
return int(f.local[len("tcp:")
|
|
400
|
+
return int(f.local[len("tcp:"):])
|
|
328
401
|
local_port = get_free_port()
|
|
329
402
|
self.forward("tcp:" + str(local_port), remote)
|
|
330
403
|
logger.debug(f"forwading port: tcp:{local_port} -> {remote}")
|
|
331
404
|
return local_port
|
|
332
405
|
|
|
333
|
-
|
|
334
|
-
def selector_to_xpath(selector: u2.Selector, is_initial: bool = True) -> str:
|
|
335
|
-
"""
|
|
336
|
-
Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
|
|
337
|
-
|
|
338
|
-
Args:
|
|
339
|
-
selector (u2.Selector): A u2 Selector object
|
|
340
|
-
is_initial (bool): Whether it is the initial node, defaults to True
|
|
341
|
-
|
|
342
|
-
Returns:
|
|
343
|
-
str: The corresponding XPath expression
|
|
344
|
-
"""
|
|
345
|
-
try:
|
|
346
|
-
if is_initial:
|
|
347
|
-
xpath = ".//node"
|
|
348
|
-
else:
|
|
349
|
-
xpath = "node"
|
|
350
|
-
|
|
351
|
-
conditions = []
|
|
352
|
-
|
|
353
|
-
if "className" in selector:
|
|
354
|
-
conditions.insert(0, f"[@class='{selector['className']}']") # 将 className 条件放在前面
|
|
355
|
-
|
|
356
|
-
if "text" in selector:
|
|
357
|
-
conditions.append(f"[@text='{selector['text']}']")
|
|
358
|
-
elif "textContains" in selector:
|
|
359
|
-
conditions.append(f"[contains(@text, '{selector['textContains']}')]")
|
|
360
|
-
elif "textMatches" in selector:
|
|
361
|
-
conditions.append(f"[re:match(@text, '{selector['textMatches']}')]")
|
|
362
|
-
elif "textStartsWith" in selector:
|
|
363
|
-
conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
|
|
364
|
-
|
|
365
|
-
if "description" in selector:
|
|
366
|
-
conditions.append(f"[@content-desc='{selector['description']}']")
|
|
367
|
-
elif "descriptionContains" in selector:
|
|
368
|
-
conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
|
|
369
|
-
elif "descriptionMatches" in selector:
|
|
370
|
-
conditions.append(f"[re:match(@content-desc, '{selector['descriptionMatches']}')]")
|
|
371
|
-
elif "descriptionStartsWith" in selector:
|
|
372
|
-
conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
|
|
373
|
-
|
|
374
|
-
if "packageName" in selector:
|
|
375
|
-
conditions.append(f"[@package='{selector['packageName']}']")
|
|
376
|
-
elif "packageNameMatches" in selector:
|
|
377
|
-
conditions.append(f"[re:match(@package, '{selector['packageNameMatches']}')]")
|
|
378
|
-
|
|
379
|
-
if "resourceId" in selector:
|
|
380
|
-
conditions.append(f"[@resource-id='{selector['resourceId']}']")
|
|
381
|
-
elif "resourceIdMatches" in selector:
|
|
382
|
-
conditions.append(f"[re:match(@resource-id, '{selector['resourceIdMatches']}')]")
|
|
383
|
-
|
|
384
|
-
bool_props = [
|
|
385
|
-
"checkable", "checked", "clickable", "longClickable", "scrollable",
|
|
386
|
-
"enabled", "focusable", "focused", "selected", "covered"
|
|
387
|
-
]
|
|
388
|
-
for prop in bool_props:
|
|
389
|
-
if prop in selector:
|
|
390
|
-
value = "true" if selector[prop] else "false"
|
|
391
|
-
conditions.append(f"[@{prop}='{value}']")
|
|
392
|
-
|
|
393
|
-
if "index" in selector:
|
|
394
|
-
conditions.append(f"[@index='{selector['index']}']")
|
|
395
|
-
elif "instance" in selector:
|
|
396
|
-
conditions.append(f"[@instance='{selector['instance']}']")
|
|
397
|
-
|
|
398
|
-
xpath += "".join(conditions)
|
|
399
|
-
|
|
400
|
-
if "childOrSibling" in selector and selector["childOrSibling"]:
|
|
401
|
-
for i, relation in enumerate(selector["childOrSibling"]):
|
|
402
|
-
sub_selector = selector["childOrSiblingSelector"][i]
|
|
403
|
-
sub_xpath = selector_to_xpath(sub_selector, False) # 递归处理子选择器
|
|
404
|
-
|
|
405
|
-
if relation == "child":
|
|
406
|
-
xpath += f"/{sub_xpath}"
|
|
407
|
-
elif relation == "sibling":
|
|
408
|
-
xpath_initial = xpath
|
|
409
|
-
xpath = '(' + xpath_initial + f"/following-sibling::{sub_xpath} | " + xpath_initial + f"/preceding-sibling::{sub_xpath})"
|
|
410
|
-
|
|
411
|
-
return xpath
|
|
412
|
-
|
|
413
|
-
except Exception as e:
|
|
414
|
-
print(f"Error occurred during selector conversion: {e}")
|
|
415
|
-
return "//error"
|
|
416
|
-
|
|
417
406
|
def is_port_in_use(port: int) -> bool:
|
|
418
407
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
419
408
|
return s.connect_ex(('127.0.0.1', port)) == 0
|
|
@@ -435,3 +424,25 @@ def get_free_port():
|
|
|
435
424
|
if not is_port_in_use(port):
|
|
436
425
|
return port
|
|
437
426
|
raise RuntimeError("No free port found")
|
|
427
|
+
|
|
428
|
+
def set_covered_to_deepest_node(selector: u2.Selector):
|
|
429
|
+
|
|
430
|
+
def find_deepest_nodes(node):
|
|
431
|
+
deepest_node = None
|
|
432
|
+
is_leaf = True
|
|
433
|
+
if "childOrSibling" in node and node["childOrSibling"]:
|
|
434
|
+
for i, relation in enumerate(node["childOrSibling"]):
|
|
435
|
+
sub_selector = node["childOrSiblingSelector"][i]
|
|
436
|
+
deepest_node = find_deepest_nodes(sub_selector)
|
|
437
|
+
is_leaf = False
|
|
438
|
+
|
|
439
|
+
if is_leaf:
|
|
440
|
+
deepest_node = node
|
|
441
|
+
return deepest_node
|
|
442
|
+
|
|
443
|
+
deepest_node = find_deepest_nodes(selector)
|
|
444
|
+
|
|
445
|
+
if deepest_node is not None:
|
|
446
|
+
dict.update(deepest_node, {"covered": False})
|
|
447
|
+
|
|
448
|
+
|
kea2/utils.py
CHANGED
|
@@ -13,6 +13,7 @@ def getLogger(name: str) -> logging.Logger:
|
|
|
13
13
|
if not logger.handlers:
|
|
14
14
|
# Configure handler
|
|
15
15
|
handler = logging.StreamHandler()
|
|
16
|
+
handler.flush = lambda: handler.stream.flush() # 确保每次都flush
|
|
16
17
|
formatter = logging.Formatter('[%(levelname)1s][%(asctime)s %(module)s:%(lineno)d pid:%(process)d] %(message)s')
|
|
17
18
|
handler.setFormatter(formatter)
|
|
18
19
|
logger.addHandler(handler)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Kea2-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A python library for supporting and customizing automated UI testing for mobile apps
|
|
5
5
|
Author-email: Xixian Liang <xixian@stu.ecnu.edu.cn>
|
|
6
6
|
Requires-Python: >=3.8
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: rtree>=1.3.0
|
|
10
|
-
Requires-Dist: uiautomator2>=3.2.9
|
|
11
10
|
Requires-Dist: jinja2>=3.0.0
|
|
11
|
+
Requires-Dist: uiautomator2>=3.3.3
|
|
12
|
+
Requires-Dist: adbutils>=2.9.3
|
|
12
13
|
Dynamic: license-file
|
|
13
14
|
|
|
14
15
|
|
|
@@ -17,25 +18,41 @@ Dynamic: license-file
|
|
|
17
18
|
[](https://pepy.tech/projects/kea2-python)
|
|
18
19
|

|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
<div>
|
|
21
|
-
<img src="https://github.com/user-attachments/assets/
|
|
23
|
+
<img src="https://github.com/user-attachments/assets/aa5839fc-4542-46f6-918b-c9f891356c84" style="border-radius: 14px; width: 20%; height: 20%;"/>
|
|
22
24
|
</div>
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
### Github repo link
|
|
27
|
+
[https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
|
|
28
|
+
|
|
29
|
+
### [点击此处:查看中文文档](README_cn.md)
|
|
30
|
+
|
|
31
|
+
## About
|
|
25
32
|
|
|
26
|
-
Kea2 is an easy-to-use
|
|
33
|
+
Kea2 is an easy-to-use tool for fuzzing mobile apps. Its key *novelty* is able to fuse automated UI testing with scripts (usually written by human), thus empowering automated UI testing with human intelligence for effectively finding *crashing bugs* as well as *non-crashing functional (logic) bugs*.
|
|
27
34
|
|
|
28
|
-
Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot_Android) and [uiautomator2](https://github.com/openatx/uiautomator2) and
|
|
35
|
+
Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot_Android), *an industrial-strength automated UI testing tool*, and [uiautomator2](https://github.com/openatx/uiautomator2), *an easy-to-use and stable Android automation library*.
|
|
36
|
+
Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
|
|
37
|
+
|
|
38
|
+
## Novelty & Important features
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
<div style="max-width:80%; max-height:80%">
|
|
42
|
+
<img src="docs/intro.png" style="border-radius: 14px; width: 80%; height: 80%;"/>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
29
45
|
|
|
30
|
-
## Important features
|
|
31
46
|
- **Feature 1**(查找稳定性问题): coming with the full capability of [Fastbot](https://github.com/bytedance/Fastbot_Android) for stress testing and finding *stability problems* (i.e., *crashing bugs*);
|
|
32
47
|
|
|
33
48
|
- **Feature 2**(自定义测试场景\事件序列\黑白名单\黑白控件[^1]): customizing testing scenarios when running Fastbot (e.g., testing specific app functionalities, executing specific event traces, entering specifc UI pages, reaching specific app states, blacklisting specific activities/UI widgets/UI regions) with the full capability and flexibility powered by *python* language and [uiautomator2](https://github.com/openatx/uiautomator2);
|
|
34
49
|
|
|
35
|
-
- **Feature 3**(支持断言机制[^2]): supporting auto-assertions when running Fastbot, based on the idea of [property-based testing](https://en.wikipedia.org/wiki/Software_testing#Property_testing) inheritted from [Kea](https://github.com/ecnusse/Kea), for finding *logic bugs* (i.e., *non-crashing bugs*)
|
|
50
|
+
- **Feature 3**(支持断言机制[^2]): supporting auto-assertions when running Fastbot, based on the idea of [property-based testing](https://en.wikipedia.org/wiki/Software_testing#Property_testing) inheritted from [Kea](https://github.com/ecnusse/Kea), for finding *logic bugs* (i.e., *non-crashing functional bugs*).
|
|
36
51
|
|
|
52
|
+
For **Feature 2 and 3**, Kea2 allows you to focus on what app functionalities to be tested. You do not need to worry about how to reach these app functionalities. Just let Fastbot help. As a result, your scripts are usually short, robust and easy to maintain, and the corresponding app functionalities are much more stress-tested!
|
|
37
53
|
|
|
38
54
|
**The ability of the three features in Kea2**
|
|
55
|
+
|
|
39
56
|
| | **Feature 1** | **Feature 2** | **Feature 3** |
|
|
40
57
|
| --- | --- | --- | ---- |
|
|
41
58
|
| **Finding crashes** | :+1: | :+1: | :+1: |
|
|
@@ -43,24 +60,17 @@ Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot
|
|
|
43
60
|
| **Finding non-crashing functional (logic) bugs** | | | :+1: |
|
|
44
61
|
|
|
45
62
|
|
|
46
|
-
<div align="center">
|
|
47
|
-
<div style="max-width:80%; max-height:80%">
|
|
48
|
-
<img src="docs/intro.png" style="border-radius: 14px; width: 80%; height: 80%;"/>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
|
|
53
63
|
|
|
54
64
|
## Design & Roadmap
|
|
55
|
-
Kea2
|
|
56
|
-
- [unittest](https://docs.python.org/3/library/unittest.html) as the testing framework;
|
|
65
|
+
Kea2 currently works with:
|
|
66
|
+
- [unittest](https://docs.python.org/3/library/unittest.html) as the testing framework to manage the scripts;
|
|
57
67
|
- [uiautomator2](https://github.com/openatx/uiautomator2) as the UI test driver;
|
|
58
68
|
- [Fastbot](https://github.com/bytedance/Fastbot_Android) as the backend automated UI testing tool.
|
|
59
69
|
|
|
60
70
|
In the future, Kea2 will be extended to support
|
|
61
|
-
- [pytest](https://docs.pytest.org/en/stable/)
|
|
62
|
-
- [Appium](https://github.com/appium/appium), [Hypium](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hypium-python-guidelines) (for HarmonyOS/Open Harmony)
|
|
63
|
-
- other automated UI testing tools (not limited to Fastbot)
|
|
71
|
+
- [pytest](https://docs.pytest.org/en/stable/), another popular python testing framework;
|
|
72
|
+
- [Appium](https://github.com/appium/appium), [Hypium](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hypium-python-guidelines) (for HarmonyOS/Open Harmony);
|
|
73
|
+
- any other automated UI testing tools (not limited to Fastbot)
|
|
64
74
|
|
|
65
75
|
|
|
66
76
|
## Installation
|
|
@@ -114,7 +124,7 @@ Test your app with the full capability of Fastbot for stress testing and finding
|
|
|
114
124
|
kea2 run -s "emulator-5554" -p it.feio.android.omninotes.alpha --agent native --running-minutes 10 --throttle 200
|
|
115
125
|
```
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
To understand the meanings of the options, you can see our [manual](docs/manual_en.md#launching-kea2).
|
|
118
128
|
|
|
119
129
|
> The usage is similar to the the original Fastbot's [shell commands](https://github.com/bytedance/Fastbot_Android?tab=readme-ov-file#run-fastbot-with-shell-command).
|
|
120
130
|
|
|
@@ -177,13 +187,14 @@ In Feature 3, a script is composed of three elements:
|
|
|
177
187
|
|
|
178
188
|
In a social media app, message sending is a common feature. On the message sending page, the `send` button should always appears when the input box is not empty (i.e., has some message).
|
|
179
189
|
|
|
180
|
-
<div align="center"
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
<div align="center">
|
|
191
|
+
<img src="docs/socialAppBug.png" style="border-radius: 14px; width:30%; height:40%;"/>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div align="center">
|
|
195
|
+
The expected behavior (the upper figure) and the buggy behavior (the lower figure).
|
|
186
196
|
</div>
|
|
197
|
+
|
|
187
198
|
|
|
188
199
|
For the preceding always-holding property, we can write the following script to validate the functional correctness: when there is an `input_box` widget on the message sending page, we can type any non-empty string text into the input box and assert `send_button` should always exists.
|
|
189
200
|
|
|
@@ -208,12 +219,12 @@ You can run this example by using the similar command line in Feature 2.
|
|
|
208
219
|
|
|
209
220
|
## Documentations(更多文档)
|
|
210
221
|
|
|
211
|
-
[
|
|
212
|
-
- Kea2
|
|
213
|
-
- Kea2
|
|
214
|
-
- Kea2
|
|
215
|
-
-
|
|
216
|
-
- [
|
|
222
|
+
You can find the [user manual](docs/manual_en.md), which includes:
|
|
223
|
+
- Examples of using Kea2 on WeChat (in Chinese);
|
|
224
|
+
- How to define Kea2's scripts and use the decorators (e.g., `@precondition`、`@prob`、`@max_tries`);
|
|
225
|
+
- How to run Kea2 and Kea2's command line options
|
|
226
|
+
- How to find and understand Kea2's testing results
|
|
227
|
+
- How to [whitelist or blacklist](docs/blacklisting.md) specific activities, UI widgets and UI regions during fuzzing
|
|
217
228
|
|
|
218
229
|
## Open-source projects used by Kea2
|
|
219
230
|
|