Kea2-python 0.0.1b3__py3-none-any.whl → 0.1.0b0__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 +1 -4
- kea2/adbUtils.py +0 -1
- kea2/assets/monkeyq.jar +0 -0
- kea2/keaUtils.py +85 -23
- kea2/kea_launcher.py +16 -4
- kea2/logWatcher.py +4 -3
- kea2/resultSyncer.py +56 -0
- kea2/u2Driver.py +90 -0
- kea2/utils.py +8 -1
- kea2_python-0.1.0b0.dist-info/METADATA +257 -0
- {kea2_python-0.0.1b3.dist-info → kea2_python-0.1.0b0.dist-info}/RECORD +15 -15
- kea2/assets/u2.jar +0 -0
- kea2_python-0.0.1b3.dist-info/METADATA +0 -506
- {kea2_python-0.0.1b3.dist-info → kea2_python-0.1.0b0.dist-info}/WHEEL +0 -0
- {kea2_python-0.0.1b3.dist-info → kea2_python-0.1.0b0.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.0.1b3.dist-info → kea2_python-0.1.0b0.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.0.1b3.dist-info → kea2_python-0.1.0b0.dist-info}/top_level.txt +0 -0
kea2/__init__.py
CHANGED
kea2/adbUtils.py
CHANGED
kea2/assets/monkeyq.jar
CHANGED
|
Binary file
|
kea2/keaUtils.py
CHANGED
|
@@ -14,9 +14,10 @@ from .absDriver import AbstractDriver
|
|
|
14
14
|
from functools import wraps
|
|
15
15
|
from time import sleep
|
|
16
16
|
from .adbUtils import push_file
|
|
17
|
+
from .resultSyncer import ResultSyncer
|
|
17
18
|
from .logWatcher import LogWatcher
|
|
18
19
|
from .utils import TimeStamp, getProjectRoot, getLogger
|
|
19
|
-
from .u2Driver import StaticU2UiObject
|
|
20
|
+
from .u2Driver import StaticU2UiObject, selector_to_xpath
|
|
20
21
|
import uiautomator2 as u2
|
|
21
22
|
import types
|
|
22
23
|
|
|
@@ -32,8 +33,8 @@ PropName = NewType("PropName", str)
|
|
|
32
33
|
PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
|
|
33
34
|
|
|
34
35
|
STAMP = TimeStamp().getTimeStamp()
|
|
35
|
-
LOGFILE
|
|
36
|
-
RESFILE
|
|
36
|
+
LOGFILE: str
|
|
37
|
+
RESFILE: str
|
|
37
38
|
|
|
38
39
|
def precondition(precond: Callable[[Any], bool]) -> Callable:
|
|
39
40
|
"""the decorator @precondition
|
|
@@ -120,7 +121,11 @@ class Options:
|
|
|
120
121
|
# the stamp for log file and result file, default: current time stamp
|
|
121
122
|
log_stamp: str = None
|
|
122
123
|
# the profiling period to get the coverage result.
|
|
123
|
-
profile_period: int =
|
|
124
|
+
profile_period: int = 25
|
|
125
|
+
# take screenshots for every step
|
|
126
|
+
take_screenshots: bool = False
|
|
127
|
+
# the debug mode
|
|
128
|
+
debug: bool = False
|
|
124
129
|
|
|
125
130
|
def __setattr__(self, name, value):
|
|
126
131
|
if value is None:
|
|
@@ -130,10 +135,21 @@ class Options:
|
|
|
130
135
|
def __post_init__(self):
|
|
131
136
|
if self.serial and self.Driver:
|
|
132
137
|
self.Driver.setDeviceSerial(self.serial)
|
|
138
|
+
global LOGFILE, RESFILE, STAMP
|
|
133
139
|
if self.log_stamp:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
STAMP = self.log_stamp
|
|
141
|
+
self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
|
|
142
|
+
LOGFILE = f"fastbot_{STAMP}.log"
|
|
143
|
+
RESFILE = f"result_{STAMP}.json"
|
|
144
|
+
|
|
145
|
+
self.profile_period = int(self.profile_period)
|
|
146
|
+
if self.profile_period < 1:
|
|
147
|
+
raise ValueError("--profile-period should be greater than 0")
|
|
148
|
+
|
|
149
|
+
self.throttle = int(self.throttle)
|
|
150
|
+
if self.throttle < 0:
|
|
151
|
+
raise ValueError("--throttle should be greater than or equal to 0")
|
|
152
|
+
|
|
137
153
|
_check_package_installation(self.serial, self.packageNames)
|
|
138
154
|
|
|
139
155
|
|
|
@@ -168,6 +184,11 @@ def getFullPropName(testCase: TestCase):
|
|
|
168
184
|
|
|
169
185
|
class JsonResult(TextTestResult):
|
|
170
186
|
res: PBTTestResult
|
|
187
|
+
|
|
188
|
+
lastExecutedInfo: Dict = {
|
|
189
|
+
"propName": "",
|
|
190
|
+
"state": "",
|
|
191
|
+
}
|
|
171
192
|
|
|
172
193
|
@classmethod
|
|
173
194
|
def setProperties(cls, allProperties: Dict):
|
|
@@ -184,6 +205,10 @@ class JsonResult(TextTestResult):
|
|
|
184
205
|
|
|
185
206
|
def addExcuted(self, test: TestCase):
|
|
186
207
|
self.res[getFullPropName(test)].executed += 1
|
|
208
|
+
self.lastExecutedInfo = {
|
|
209
|
+
"propName": getFullPropName(test),
|
|
210
|
+
"state": "start",
|
|
211
|
+
}
|
|
187
212
|
|
|
188
213
|
def addPrecondSatisfied(self, test: TestCase):
|
|
189
214
|
self.res[getFullPropName(test)].precond_satisfied += 1
|
|
@@ -191,10 +216,12 @@ class JsonResult(TextTestResult):
|
|
|
191
216
|
def addFailure(self, test, err):
|
|
192
217
|
super().addFailure(test, err)
|
|
193
218
|
self.res[getFullPropName(test)].fail += 1
|
|
219
|
+
self.lastExecutedInfo["state"] = "fail"
|
|
194
220
|
|
|
195
221
|
def addError(self, test, err):
|
|
196
222
|
super().addError(test, err)
|
|
197
223
|
self.res[getFullPropName(test)].error += 1
|
|
224
|
+
self.lastExecutedInfo["state"] = "error"
|
|
198
225
|
|
|
199
226
|
def getExcuted(self, test: TestCase):
|
|
200
227
|
return self.res[getFullPropName(test)].executed
|
|
@@ -271,14 +298,18 @@ def startFastbotService(options: Options) -> threading.Thread:
|
|
|
271
298
|
"exec", "app_process",
|
|
272
299
|
"/system/bin", "com.android.commands.monkey.Monkey",
|
|
273
300
|
"-p", *options.packageNames,
|
|
274
|
-
"--agent-u2" if options.agent == "u2" else "--agent",
|
|
301
|
+
"--agent-u2" if options.agent == "u2" else "--agent",
|
|
275
302
|
"reuseq",
|
|
276
303
|
"--running-minutes", f"{options.running_mins}",
|
|
277
304
|
"--throttle", f"{options.throttle}",
|
|
278
|
-
"--bugreport",
|
|
279
|
-
"-v", "-v", "-v"
|
|
305
|
+
"--bugreport",
|
|
280
306
|
]
|
|
281
307
|
|
|
308
|
+
if options.profile_period:
|
|
309
|
+
shell_command += ["--profile-period", f"{options.profile_period}"]
|
|
310
|
+
|
|
311
|
+
shell_command += ["-v", "-v", "-v"]
|
|
312
|
+
|
|
282
313
|
full_cmd = ["adb"] + (["-s", options.serial] if options.serial else []) + ["shell"] + shell_command
|
|
283
314
|
|
|
284
315
|
outfile = open(LOGFILE, "w", encoding="utf-8", buffering=1)
|
|
@@ -298,7 +329,7 @@ def startFastbotService(options: Options) -> threading.Thread:
|
|
|
298
329
|
def close_on_exit(proc: subprocess.Popen, f: IO):
|
|
299
330
|
proc.wait()
|
|
300
331
|
f.close()
|
|
301
|
-
|
|
332
|
+
|
|
302
333
|
|
|
303
334
|
class KeaTestRunner(TextTestRunner):
|
|
304
335
|
|
|
@@ -371,6 +402,10 @@ class KeaTestRunner(TextTestRunner):
|
|
|
371
402
|
# setUp for the u2 driver
|
|
372
403
|
self.scriptDriver = self.options.Driver.getScriptDriver()
|
|
373
404
|
check_alive(port=self.scriptDriver.lport)
|
|
405
|
+
self._init()
|
|
406
|
+
|
|
407
|
+
resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
|
|
408
|
+
resultSyncer.run()
|
|
374
409
|
|
|
375
410
|
end_by_remote = False
|
|
376
411
|
self.stepsCount = 0
|
|
@@ -385,7 +420,6 @@ class KeaTestRunner(TextTestRunner):
|
|
|
385
420
|
|
|
386
421
|
try:
|
|
387
422
|
xml_raw = self.stepMonkey()
|
|
388
|
-
stat = self._getStat()
|
|
389
423
|
propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
|
|
390
424
|
except requests.ConnectionError:
|
|
391
425
|
print(
|
|
@@ -394,6 +428,9 @@ class KeaTestRunner(TextTestRunner):
|
|
|
394
428
|
end_by_remote = True
|
|
395
429
|
break
|
|
396
430
|
|
|
431
|
+
if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
|
|
432
|
+
resultSyncer.sync_event.set()
|
|
433
|
+
|
|
397
434
|
print(f"{len(propsSatisfiedPrecond)} precond satisfied.", flush=True)
|
|
398
435
|
|
|
399
436
|
# Go to the next round if no precond satisfied
|
|
@@ -421,19 +458,23 @@ class KeaTestRunner(TextTestRunner):
|
|
|
421
458
|
print("execute property %s." % execPropName, flush=True)
|
|
422
459
|
|
|
423
460
|
result.addExcuted(test)
|
|
461
|
+
self._logScript(result.lastExecutedInfo)
|
|
424
462
|
try:
|
|
425
463
|
test(result)
|
|
426
464
|
finally:
|
|
427
465
|
result.printErrors()
|
|
428
466
|
|
|
467
|
+
self._logScript(result.lastExecutedInfo)
|
|
429
468
|
result.flushResult(outfile=RESFILE)
|
|
430
469
|
|
|
431
470
|
if not end_by_remote:
|
|
432
471
|
self.stopMonkey()
|
|
433
472
|
result.flushResult(outfile=RESFILE)
|
|
473
|
+
resultSyncer.close()
|
|
434
474
|
|
|
435
475
|
print(f"Finish sending monkey events.", flush=True)
|
|
436
476
|
log_watcher.close()
|
|
477
|
+
|
|
437
478
|
|
|
438
479
|
# Source code from unittest Runner
|
|
439
480
|
# process the result
|
|
@@ -537,14 +578,31 @@ class KeaTestRunner(TextTestRunner):
|
|
|
537
578
|
validProps[propName] = test
|
|
538
579
|
return validProps
|
|
539
580
|
|
|
540
|
-
def
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
581
|
+
def _logScript(self, execution_info:Dict):
|
|
582
|
+
URL = f"http://localhost:{self.scriptDriver.lport}/logScript"
|
|
583
|
+
r = requests.post(
|
|
584
|
+
url=URL,
|
|
585
|
+
json=execution_info
|
|
586
|
+
)
|
|
587
|
+
res = r.content.decode(encoding="utf-8")
|
|
588
|
+
if res != "OK":
|
|
589
|
+
print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
|
|
590
|
+
|
|
591
|
+
def _init(self):
|
|
592
|
+
URL = f"http://localhost:{self.scriptDriver.lport}/init"
|
|
593
|
+
data = {
|
|
594
|
+
"takeScreenshots": self.options.take_screenshots,
|
|
595
|
+
"Stamp": STAMP
|
|
596
|
+
}
|
|
597
|
+
print(f"[INFO] Init fastbot: {data}", flush=True)
|
|
598
|
+
r = requests.post(
|
|
599
|
+
url=URL,
|
|
600
|
+
json=data
|
|
601
|
+
)
|
|
602
|
+
res = r.content.decode(encoding="utf-8")
|
|
603
|
+
import re
|
|
604
|
+
self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
|
|
605
|
+
print(f"[INFO] Fastbot initiated. Device outputDir: {res}", flush=True)
|
|
548
606
|
|
|
549
607
|
def collectAllProperties(self, test: TestSuite):
|
|
550
608
|
"""collect all the properties to prepare for PBT
|
|
@@ -560,7 +618,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
560
618
|
"""remove the tearDown function in PBT
|
|
561
619
|
"""
|
|
562
620
|
def tearDown(self): ...
|
|
563
|
-
testCase = types.MethodType(tearDown, testCase)
|
|
621
|
+
testCase.tearDown = types.MethodType(tearDown, testCase)
|
|
564
622
|
|
|
565
623
|
def iter_tests(suite):
|
|
566
624
|
for test in suite:
|
|
@@ -654,8 +712,8 @@ class KeaTestRunner(TextTestRunner):
|
|
|
654
712
|
_widgets = _widgets if isinstance(_widgets, list) else [_widgets]
|
|
655
713
|
for w in _widgets:
|
|
656
714
|
if isinstance(w, StaticU2UiObject):
|
|
657
|
-
xpath =
|
|
658
|
-
blocked_set.add(xpath)
|
|
715
|
+
xpath = selector_to_xpath(w.selector, True)
|
|
716
|
+
blocked_set.add(xpath)
|
|
659
717
|
elif isinstance(w, u2.xpath.XPathSelector):
|
|
660
718
|
xpath = w._parent.xpath
|
|
661
719
|
blocked_set.add(xpath)
|
|
@@ -691,5 +749,9 @@ class KeaTestRunner(TextTestRunner):
|
|
|
691
749
|
def __del__(self):
|
|
692
750
|
"""tearDown method. Cleanup the env.
|
|
693
751
|
"""
|
|
752
|
+
try:
|
|
753
|
+
self.stopMonkey()
|
|
754
|
+
except Exception as e:
|
|
755
|
+
pass
|
|
694
756
|
if self.options.Driver:
|
|
695
757
|
self.options.Driver.tearDown()
|
kea2/kea_launcher.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import sys
|
|
3
2
|
import argparse
|
|
4
3
|
import unittest
|
|
5
|
-
from pathlib import Path
|
|
6
4
|
from typing import List
|
|
7
5
|
|
|
8
6
|
def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
@@ -49,6 +47,7 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
|
|
|
49
47
|
dest="running_minutes",
|
|
50
48
|
type=int,
|
|
51
49
|
required=False,
|
|
50
|
+
default=10,
|
|
52
51
|
help="Time to run fastbot",
|
|
53
52
|
)
|
|
54
53
|
|
|
@@ -89,8 +88,18 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
|
|
|
89
88
|
dest="profile_period",
|
|
90
89
|
type=int,
|
|
91
90
|
required=False,
|
|
91
|
+
default=25,
|
|
92
92
|
help="Steps to profile the testing statistics.",
|
|
93
93
|
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--take-screenshots",
|
|
97
|
+
dest="take_screenshots",
|
|
98
|
+
required=False,
|
|
99
|
+
action="store_true",
|
|
100
|
+
default=False,
|
|
101
|
+
help="Take screenshots for every step.",
|
|
102
|
+
)
|
|
94
103
|
|
|
95
104
|
parser.add_argument(
|
|
96
105
|
"extra",
|
|
@@ -118,6 +127,10 @@ def driver_info_logger(args):
|
|
|
118
127
|
print(" running_minutes:", args.running_minutes, flush=True)
|
|
119
128
|
if args.throttle_ms:
|
|
120
129
|
print(" throttle_ms:", args.throttle_ms, flush=True)
|
|
130
|
+
if args.log_stamp:
|
|
131
|
+
print(" log_stamp:", args.log_stamp, flush=True)
|
|
132
|
+
if args.take_screenshots:
|
|
133
|
+
print(" take_screenshots:", args.take_screenshots, flush=True)
|
|
121
134
|
|
|
122
135
|
|
|
123
136
|
def parse_args(argv: List):
|
|
@@ -140,9 +153,7 @@ def _sanitize_args(args):
|
|
|
140
153
|
def run(args=None):
|
|
141
154
|
if args is None:
|
|
142
155
|
args = parse_args(sys.argv[1:])
|
|
143
|
-
|
|
144
156
|
_sanitize_args(args)
|
|
145
|
-
|
|
146
157
|
driver_info_logger(args)
|
|
147
158
|
unittest_info_logger(args)
|
|
148
159
|
|
|
@@ -159,6 +170,7 @@ def run(args=None):
|
|
|
159
170
|
throttle=args.throttle_ms,
|
|
160
171
|
log_stamp=args.log_stamp,
|
|
161
172
|
profile_period=args.profile_period,
|
|
173
|
+
take_screenshots=args.take_screenshots,
|
|
162
174
|
)
|
|
163
175
|
|
|
164
176
|
KeaTestRunner.setOptions(options)
|
kea2/logWatcher.py
CHANGED
|
@@ -20,10 +20,13 @@ class LogWatcher:
|
|
|
20
20
|
self.buffer = ""
|
|
21
21
|
self.last_pos = 0
|
|
22
22
|
|
|
23
|
-
while
|
|
23
|
+
while not self.end_flag:
|
|
24
24
|
self.read_log()
|
|
25
25
|
time.sleep(poll_interval)
|
|
26
26
|
|
|
27
|
+
time.sleep(0.2)
|
|
28
|
+
self.read_log()
|
|
29
|
+
|
|
27
30
|
def read_log(self):
|
|
28
31
|
time.sleep(0.02)
|
|
29
32
|
with open(self.log_file, 'r', encoding='utf-8') as f:
|
|
@@ -65,9 +68,7 @@ class LogWatcher:
|
|
|
65
68
|
t.start()
|
|
66
69
|
|
|
67
70
|
def close(self):
|
|
68
|
-
time.sleep(0.2) # wait for the written logfile close
|
|
69
71
|
self.end_flag = True
|
|
70
|
-
self.read_log()
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
if __name__ == "__main__":
|
kea2/resultSyncer.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from .adbUtils import adb_shell, pull_file
|
|
3
|
+
from .utils import getLogger
|
|
4
|
+
|
|
5
|
+
logger = getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResultSyncer:
|
|
9
|
+
|
|
10
|
+
def __init__(self, device_output_dir, output_dir):
|
|
11
|
+
self.device_output_dir = device_output_dir
|
|
12
|
+
self.output_dir = output_dir
|
|
13
|
+
self.running = False
|
|
14
|
+
self.thread = None
|
|
15
|
+
self.sync_event = threading.Event()
|
|
16
|
+
|
|
17
|
+
def run(self):
|
|
18
|
+
"""Start a background thread to sync device data when triggered"""
|
|
19
|
+
self.running = True
|
|
20
|
+
self.thread = threading.Thread(target=self._sync_thread, daemon=True)
|
|
21
|
+
self.thread.start()
|
|
22
|
+
|
|
23
|
+
def _sync_thread(self):
|
|
24
|
+
"""Thread function that waits for sync event and then syncs data"""
|
|
25
|
+
while self.running:
|
|
26
|
+
# Wait for sync event with a timeout to periodically check if still running
|
|
27
|
+
if self.sync_event.wait(timeout=3):
|
|
28
|
+
self._sync_device_data()
|
|
29
|
+
self.sync_event.clear()
|
|
30
|
+
|
|
31
|
+
def close(self):
|
|
32
|
+
self.running = False
|
|
33
|
+
self.sync_event.set()
|
|
34
|
+
if self.thread and self.thread.is_alive():
|
|
35
|
+
self.thread.join(timeout=10)
|
|
36
|
+
self._sync_device_data()
|
|
37
|
+
try:
|
|
38
|
+
logger.debug(f"Removing device output directory: {self.device_output_dir}")
|
|
39
|
+
remove_device_dir = ["rm", "-rf", self.device_output_dir]
|
|
40
|
+
adb_shell(remove_device_dir)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Error removing device output directory: {e}", flush=True)
|
|
43
|
+
|
|
44
|
+
def _sync_device_data(self):
|
|
45
|
+
"""
|
|
46
|
+
Sync the device data to the local directory.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
logger.debug("Syncing data")
|
|
50
|
+
|
|
51
|
+
pull_file(self.device_output_dir, str(self.output_dir))
|
|
52
|
+
|
|
53
|
+
remove_pulled_screenshots = ["find", self.device_output_dir, "-name", "\"*.png\"", "-delete"]
|
|
54
|
+
adb_shell(remove_pulled_screenshots)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Error in data sync: {e}", flush=True)
|
kea2/u2Driver.py
CHANGED
|
@@ -123,6 +123,7 @@ class StaticU2UiObject(u2.UiObject):
|
|
|
123
123
|
xpath = f".//node{''.join(attrLocs)}"
|
|
124
124
|
return xpath
|
|
125
125
|
|
|
126
|
+
|
|
126
127
|
@property
|
|
127
128
|
def exists(self):
|
|
128
129
|
dict.update(self.selector, {"covered": "false"})
|
|
@@ -134,6 +135,12 @@ class StaticU2UiObject(u2.UiObject):
|
|
|
134
135
|
xpath = self._getXPath(self.selector)
|
|
135
136
|
matched_widgets = self.session.xml.xpath(xpath)
|
|
136
137
|
return len(matched_widgets)
|
|
138
|
+
|
|
139
|
+
def child(self, **kwargs):
|
|
140
|
+
return StaticU2UiObject(self.session, self.selector.clone().child(**kwargs))
|
|
141
|
+
|
|
142
|
+
def sibling(self, **kwargs):
|
|
143
|
+
return StaticU2UiObject(self.session, self.selector.clone().sibling(**kwargs))
|
|
137
144
|
|
|
138
145
|
|
|
139
146
|
def _get_bounds(raw_bounds):
|
|
@@ -324,6 +331,89 @@ def forward_port(self, remote: Union[int, str]) -> int:
|
|
|
324
331
|
return local_port
|
|
325
332
|
|
|
326
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
|
+
|
|
327
417
|
def is_port_in_use(port: int) -> bool:
|
|
328
418
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
329
419
|
return s.connect_ex(('127.0.0.1', port)) == 0
|
kea2/utils.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .keaUtils import Options
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def getLogger(name: str) -> logging.Logger:
|
|
@@ -50,4 +53,8 @@ def getProjectRoot():
|
|
|
50
53
|
if cur_dir == root:
|
|
51
54
|
return None
|
|
52
55
|
cur_dir = cur_dir.parent
|
|
53
|
-
return cur_dir
|
|
56
|
+
return cur_dir
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|