Kea2-python 0.1.0b0__py3-none-any.whl → 0.1.2__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/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
- kea2/assets/kea2-thirdpart.jar +0 -0
- kea2/assets/monkeyq.jar +0 -0
- kea2/assets/quicktest.py +3 -1
- kea2/bug_report_generator.py +479 -0
- kea2/fastbotManager.py +155 -0
- kea2/keaUtils.py +56 -133
- kea2/kea_launcher.py +10 -0
- kea2/logWatcher.py +11 -4
- kea2/templates/bug_report_template.html +937 -0
- kea2/utils.py +1 -5
- {kea2_python-0.1.0b0.dist-info → kea2_python-0.1.2.dist-info}/METADATA +14 -17
- {kea2_python-0.1.0b0.dist-info → kea2_python-0.1.2.dist-info}/RECORD +18 -14
- {kea2_python-0.1.0b0.dist-info → kea2_python-0.1.2.dist-info}/WHEEL +0 -0
- {kea2_python-0.1.0b0.dist-info → kea2_python-0.1.2.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.1.0b0.dist-info → kea2_python-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.1.0b0.dist-info → kea2_python-0.1.2.dist-info}/top_level.txt +0 -0
kea2/keaUtils.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
import subprocess
|
|
5
|
-
import threading
|
|
6
4
|
import traceback
|
|
7
|
-
from typing import
|
|
5
|
+
from typing import Callable, Any, Dict, List, Literal, NewType, TypedDict, Union
|
|
8
6
|
from unittest import TextTestRunner, registerResult, TestSuite, TestCase, TextTestResult
|
|
9
7
|
import random
|
|
10
8
|
import warnings
|
|
@@ -12,12 +10,12 @@ from dataclasses import dataclass, asdict
|
|
|
12
10
|
import requests
|
|
13
11
|
from .absDriver import AbstractDriver
|
|
14
12
|
from functools import wraps
|
|
15
|
-
from
|
|
16
|
-
from .adbUtils import push_file
|
|
13
|
+
from .bug_report_generator import BugReportGenerator
|
|
17
14
|
from .resultSyncer import ResultSyncer
|
|
18
15
|
from .logWatcher import LogWatcher
|
|
19
16
|
from .utils import TimeStamp, getProjectRoot, getLogger
|
|
20
17
|
from .u2Driver import StaticU2UiObject, selector_to_xpath
|
|
18
|
+
from .fastbotManager import FastbotManager
|
|
21
19
|
import uiautomator2 as u2
|
|
22
20
|
import types
|
|
23
21
|
|
|
@@ -31,6 +29,10 @@ logger = getLogger(__name__)
|
|
|
31
29
|
# Class Typing
|
|
32
30
|
PropName = NewType("PropName", str)
|
|
33
31
|
PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
|
|
32
|
+
PropertyExecutionInfo = TypedDict(
|
|
33
|
+
"PropertyExecutionInfo",
|
|
34
|
+
{"propName": PropName, "state": Literal["start", "pass", "fail", "error"]}
|
|
35
|
+
)
|
|
34
36
|
|
|
35
37
|
STAMP = TimeStamp().getTimeStamp()
|
|
36
38
|
LOGFILE: str
|
|
@@ -124,6 +126,8 @@ class Options:
|
|
|
124
126
|
profile_period: int = 25
|
|
125
127
|
# take screenshots for every step
|
|
126
128
|
take_screenshots: bool = False
|
|
129
|
+
# The root of output dir on device
|
|
130
|
+
device_output_root: str = "/sdcard"
|
|
127
131
|
# the debug mode
|
|
128
132
|
debug: bool = False
|
|
129
133
|
|
|
@@ -133,10 +137,18 @@ class Options:
|
|
|
133
137
|
super().__setattr__(name, value)
|
|
134
138
|
|
|
135
139
|
def __post_init__(self):
|
|
140
|
+
import logging
|
|
141
|
+
logging.basicConfig(level=logging.DEBUG if self.debug else logging.INFO)
|
|
136
142
|
if self.serial and self.Driver:
|
|
137
143
|
self.Driver.setDeviceSerial(self.serial)
|
|
138
144
|
global LOGFILE, RESFILE, STAMP
|
|
139
145
|
if self.log_stamp:
|
|
146
|
+
illegal_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\n', '\r', '\t', '\0']
|
|
147
|
+
for char in illegal_chars:
|
|
148
|
+
if char in self.log_stamp:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"char: `{char}` is illegal in --log-stamp. current stamp: {self.log_stamp}"
|
|
151
|
+
)
|
|
140
152
|
STAMP = self.log_stamp
|
|
141
153
|
self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
|
|
142
154
|
LOGFILE = f"fastbot_{STAMP}.log"
|
|
@@ -185,7 +197,7 @@ def getFullPropName(testCase: TestCase):
|
|
|
185
197
|
class JsonResult(TextTestResult):
|
|
186
198
|
res: PBTTestResult
|
|
187
199
|
|
|
188
|
-
lastExecutedInfo:
|
|
200
|
+
lastExecutedInfo: PropertyExecutionInfo = {
|
|
189
201
|
"propName": "",
|
|
190
202
|
"state": "",
|
|
191
203
|
}
|
|
@@ -223,128 +235,27 @@ class JsonResult(TextTestResult):
|
|
|
223
235
|
self.res[getFullPropName(test)].error += 1
|
|
224
236
|
self.lastExecutedInfo["state"] = "error"
|
|
225
237
|
|
|
238
|
+
def updateExectedInfo(self):
|
|
239
|
+
if self.lastExecutedInfo["state"] == "start":
|
|
240
|
+
self.lastExecutedInfo["state"] = "pass"
|
|
241
|
+
|
|
226
242
|
def getExcuted(self, test: TestCase):
|
|
227
243
|
return self.res[getFullPropName(test)].executed
|
|
228
244
|
|
|
229
245
|
|
|
230
|
-
def activateFastbot(options: Options, port=None) -> threading.Thread:
|
|
231
|
-
"""
|
|
232
|
-
activate fastbot.
|
|
233
|
-
:params: options: the running setting for fastbot
|
|
234
|
-
:params: port: the listening port for script driver
|
|
235
|
-
:return: the fastbot daemon thread
|
|
236
|
-
"""
|
|
237
|
-
cur_dir = Path(__file__).parent
|
|
238
|
-
push_file(
|
|
239
|
-
Path.joinpath(cur_dir, "assets/monkeyq.jar"),
|
|
240
|
-
"/sdcard/monkeyq.jar",
|
|
241
|
-
device=options.serial
|
|
242
|
-
)
|
|
243
|
-
push_file(
|
|
244
|
-
Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
|
|
245
|
-
"/sdcard/fastbot-thirdpart.jar",
|
|
246
|
-
device=options.serial,
|
|
247
|
-
)
|
|
248
|
-
push_file(
|
|
249
|
-
Path.joinpath(cur_dir, "assets/framework.jar"),
|
|
250
|
-
"/sdcard/framework.jar",
|
|
251
|
-
device=options.serial
|
|
252
|
-
)
|
|
253
|
-
push_file(
|
|
254
|
-
Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a"),
|
|
255
|
-
"/data/local/tmp",
|
|
256
|
-
device=options.serial
|
|
257
|
-
)
|
|
258
|
-
push_file(
|
|
259
|
-
Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a"),
|
|
260
|
-
"/data/local/tmp",
|
|
261
|
-
device=options.serial
|
|
262
|
-
)
|
|
263
|
-
push_file(
|
|
264
|
-
Path.joinpath(cur_dir, "assets/fastbot_libs/x86"),
|
|
265
|
-
"/data/local/tmp",
|
|
266
|
-
device=options.serial
|
|
267
|
-
)
|
|
268
|
-
push_file(
|
|
269
|
-
Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64"),
|
|
270
|
-
"/data/local/tmp",
|
|
271
|
-
device=options.serial
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
t = startFastbotService(options)
|
|
275
|
-
print("[INFO] Running Fastbot...", flush=True)
|
|
276
|
-
|
|
277
|
-
return t
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def check_alive(port):
|
|
281
|
-
"""
|
|
282
|
-
check if the script driver and proxy server are alive.
|
|
283
|
-
"""
|
|
284
|
-
for _ in range(10):
|
|
285
|
-
sleep(2)
|
|
286
|
-
try:
|
|
287
|
-
requests.get(f"http://localhost:{port}/ping")
|
|
288
|
-
return
|
|
289
|
-
except requests.ConnectionError:
|
|
290
|
-
print("[INFO] waiting for connection.", flush=True)
|
|
291
|
-
pass
|
|
292
|
-
raise RuntimeError("Failed to connect fastbot")
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def startFastbotService(options: Options) -> threading.Thread:
|
|
296
|
-
shell_command = [
|
|
297
|
-
"CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar:/sdcard/fastbot-thirdpart.jar",
|
|
298
|
-
"exec", "app_process",
|
|
299
|
-
"/system/bin", "com.android.commands.monkey.Monkey",
|
|
300
|
-
"-p", *options.packageNames,
|
|
301
|
-
"--agent-u2" if options.agent == "u2" else "--agent",
|
|
302
|
-
"reuseq",
|
|
303
|
-
"--running-minutes", f"{options.running_mins}",
|
|
304
|
-
"--throttle", f"{options.throttle}",
|
|
305
|
-
"--bugreport",
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
if options.profile_period:
|
|
309
|
-
shell_command += ["--profile-period", f"{options.profile_period}"]
|
|
310
|
-
|
|
311
|
-
shell_command += ["-v", "-v", "-v"]
|
|
312
|
-
|
|
313
|
-
full_cmd = ["adb"] + (["-s", options.serial] if options.serial else []) + ["shell"] + shell_command
|
|
314
|
-
|
|
315
|
-
outfile = open(LOGFILE, "w", encoding="utf-8", buffering=1)
|
|
316
|
-
|
|
317
|
-
print("[INFO] Options info: {}".format(asdict(options)), flush=True)
|
|
318
|
-
print("[INFO] Launching fastbot with shell command:\n{}".format(" ".join(full_cmd)), flush=True)
|
|
319
|
-
print("[INFO] Fastbot log will be saved to {}".format(outfile.name), flush=True)
|
|
320
|
-
|
|
321
|
-
# process handler
|
|
322
|
-
proc = subprocess.Popen(full_cmd, stdout=outfile, stderr=outfile)
|
|
323
|
-
t = threading.Thread(target=close_on_exit, args=(proc, outfile), daemon=True)
|
|
324
|
-
t.start()
|
|
325
|
-
|
|
326
|
-
return t
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def close_on_exit(proc: subprocess.Popen, f: IO):
|
|
330
|
-
proc.wait()
|
|
331
|
-
f.close()
|
|
332
|
-
|
|
333
|
-
|
|
334
246
|
class KeaTestRunner(TextTestRunner):
|
|
335
247
|
|
|
336
248
|
resultclass: JsonResult
|
|
337
249
|
allProperties: PropertyStore
|
|
338
250
|
options: Options = None
|
|
339
251
|
_block_funcs: Dict[Literal["widgets", "trees"], List[Callable]] = None
|
|
340
|
-
# _block_trees_funcs = None
|
|
341
252
|
|
|
342
253
|
@classmethod
|
|
343
254
|
def setOptions(cls, options: Options):
|
|
344
255
|
if not isinstance(options.packageNames, list) and len(options.packageNames) > 0:
|
|
345
256
|
raise ValueError("packageNames should be given in a list.")
|
|
346
257
|
if options.Driver is not None and options.agent == "native":
|
|
347
|
-
|
|
258
|
+
logger.warning("[Warning] Can not use any Driver when runing native mode.")
|
|
348
259
|
options.Driver = None
|
|
349
260
|
cls.options = options
|
|
350
261
|
|
|
@@ -354,8 +265,8 @@ class KeaTestRunner(TextTestRunner):
|
|
|
354
265
|
global LOGFILE, RESFILE
|
|
355
266
|
LOGFILE = output_dir / Path(LOGFILE)
|
|
356
267
|
RESFILE = output_dir / Path(RESFILE)
|
|
357
|
-
logger.
|
|
358
|
-
logger.
|
|
268
|
+
logger.info(f"Log file: {LOGFILE}")
|
|
269
|
+
logger.info(f"Result file: {RESFILE}")
|
|
359
270
|
|
|
360
271
|
def run(self, test):
|
|
361
272
|
|
|
@@ -363,7 +274,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
363
274
|
self.collectAllProperties(test)
|
|
364
275
|
|
|
365
276
|
if len(self.allProperties) == 0:
|
|
366
|
-
|
|
277
|
+
logger.warning("[Warning] No property has been found.")
|
|
367
278
|
|
|
368
279
|
self._setOuputDir()
|
|
369
280
|
|
|
@@ -392,16 +303,17 @@ class KeaTestRunner(TextTestRunner):
|
|
|
392
303
|
message=r"Please use assert\w+ instead.",
|
|
393
304
|
)
|
|
394
305
|
|
|
395
|
-
|
|
306
|
+
fb = FastbotManager(self.options, LOGFILE)
|
|
307
|
+
fb.start()
|
|
308
|
+
|
|
396
309
|
log_watcher = LogWatcher(LOGFILE)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
else:
|
|
310
|
+
|
|
311
|
+
if self.options.agent == "u2":
|
|
400
312
|
# initialize the result.json file
|
|
401
313
|
result.flushResult(outfile=RESFILE)
|
|
402
314
|
# setUp for the u2 driver
|
|
403
315
|
self.scriptDriver = self.options.Driver.getScriptDriver()
|
|
404
|
-
check_alive(port=self.scriptDriver.lport)
|
|
316
|
+
fb.check_alive(port=self.scriptDriver.lport)
|
|
405
317
|
self._init()
|
|
406
318
|
|
|
407
319
|
resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
|
|
@@ -412,21 +324,22 @@ class KeaTestRunner(TextTestRunner):
|
|
|
412
324
|
while self.stepsCount < self.options.maxStep:
|
|
413
325
|
|
|
414
326
|
self.stepsCount += 1
|
|
415
|
-
|
|
327
|
+
logger.info("Sending monkeyEvent {}".format(
|
|
416
328
|
f"({self.stepsCount} / {self.options.maxStep})" if self.options.maxStep != float("inf")
|
|
417
329
|
else f"({self.stepsCount})"
|
|
418
330
|
)
|
|
419
|
-
|
|
331
|
+
)
|
|
420
332
|
|
|
421
333
|
try:
|
|
422
334
|
xml_raw = self.stepMonkey()
|
|
423
335
|
propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
|
|
424
336
|
except requests.ConnectionError:
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
337
|
+
logger.info("Connection refused by remote.")
|
|
338
|
+
if fb.get_return_code() == 0:
|
|
339
|
+
logger.info("Exploration times up (--running-minutes).")
|
|
340
|
+
end_by_remote = True
|
|
341
|
+
break
|
|
342
|
+
raise RuntimeError("Fastbot Aborted.")
|
|
430
343
|
|
|
431
344
|
if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
|
|
432
345
|
resultSyncer.sync_event.set()
|
|
@@ -464,17 +377,18 @@ class KeaTestRunner(TextTestRunner):
|
|
|
464
377
|
finally:
|
|
465
378
|
result.printErrors()
|
|
466
379
|
|
|
380
|
+
result.updateExectedInfo()
|
|
467
381
|
self._logScript(result.lastExecutedInfo)
|
|
468
382
|
result.flushResult(outfile=RESFILE)
|
|
469
383
|
|
|
470
384
|
if not end_by_remote:
|
|
471
385
|
self.stopMonkey()
|
|
472
386
|
result.flushResult(outfile=RESFILE)
|
|
473
|
-
resultSyncer.close()
|
|
474
|
-
|
|
387
|
+
resultSyncer.close()
|
|
388
|
+
|
|
389
|
+
fb.join()
|
|
475
390
|
print(f"Finish sending monkey events.", flush=True)
|
|
476
391
|
log_watcher.close()
|
|
477
|
-
|
|
478
392
|
|
|
479
393
|
# Source code from unittest Runner
|
|
480
394
|
# process the result
|
|
@@ -592,7 +506,8 @@ class KeaTestRunner(TextTestRunner):
|
|
|
592
506
|
URL = f"http://localhost:{self.scriptDriver.lport}/init"
|
|
593
507
|
data = {
|
|
594
508
|
"takeScreenshots": self.options.take_screenshots,
|
|
595
|
-
"Stamp": STAMP
|
|
509
|
+
"Stamp": STAMP,
|
|
510
|
+
"deviceOutputRoot": self.options.device_output_root,
|
|
596
511
|
}
|
|
597
512
|
print(f"[INFO] Init fastbot: {data}", flush=True)
|
|
598
513
|
r = requests.post(
|
|
@@ -602,7 +517,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
602
517
|
res = r.content.decode(encoding="utf-8")
|
|
603
518
|
import re
|
|
604
519
|
self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
|
|
605
|
-
print(f"[INFO] Fastbot initiated.
|
|
520
|
+
print(f"[INFO] Fastbot initiated. outputDir: {res}", flush=True)
|
|
606
521
|
|
|
607
522
|
def collectAllProperties(self, test: TestSuite):
|
|
608
523
|
"""collect all the properties to prepare for PBT
|
|
@@ -619,7 +534,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
619
534
|
"""
|
|
620
535
|
def tearDown(self): ...
|
|
621
536
|
testCase.tearDown = types.MethodType(tearDown, testCase)
|
|
622
|
-
|
|
537
|
+
|
|
623
538
|
def iter_tests(suite):
|
|
624
539
|
for test in suite:
|
|
625
540
|
if isinstance(test, TestSuite):
|
|
@@ -753,5 +668,13 @@ class KeaTestRunner(TextTestRunner):
|
|
|
753
668
|
self.stopMonkey()
|
|
754
669
|
except Exception as e:
|
|
755
670
|
pass
|
|
671
|
+
|
|
756
672
|
if self.options.Driver:
|
|
757
673
|
self.options.Driver.tearDown()
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
logger.info("Generating bug report")
|
|
677
|
+
report_generator = BugReportGenerator(self.options.output_dir)
|
|
678
|
+
report_generator.generate_report()
|
|
679
|
+
except Exception as e:
|
|
680
|
+
logger.error(f"Error generating bug report: {e}", flush=True)
|
kea2/kea_launcher.py
CHANGED
|
@@ -91,6 +91,15 @@ def _set_runner_parser(subparsers: "argparse._SubParsersAction[argparse.Argument
|
|
|
91
91
|
default=25,
|
|
92
92
|
help="Steps to profile the testing statistics.",
|
|
93
93
|
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--device-output-root",
|
|
97
|
+
dest="device_output_root",
|
|
98
|
+
type=str,
|
|
99
|
+
required=False,
|
|
100
|
+
default="/sdcard",
|
|
101
|
+
help="The root of device output dir. (Saving tmp log files and screenshots)",
|
|
102
|
+
)
|
|
94
103
|
|
|
95
104
|
parser.add_argument(
|
|
96
105
|
"--take-screenshots",
|
|
@@ -171,6 +180,7 @@ def run(args=None):
|
|
|
171
180
|
log_stamp=args.log_stamp,
|
|
172
181
|
profile_period=args.profile_period,
|
|
173
182
|
take_screenshots=args.take_screenshots,
|
|
183
|
+
device_output_root=args.device_output_root
|
|
174
184
|
)
|
|
175
185
|
|
|
176
186
|
KeaTestRunner.setOptions(options)
|
kea2/logWatcher.py
CHANGED
|
@@ -2,6 +2,10 @@ import re
|
|
|
2
2
|
import os
|
|
3
3
|
import threading
|
|
4
4
|
import time
|
|
5
|
+
from .utils import getLogger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = getLogger(__name__)
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
PATTERN_EXCEPTION = re.compile(r"\[Fastbot\].+Internal\serror\n([\s\S]*)")
|
|
@@ -16,7 +20,7 @@ def thread_excepthook(args):
|
|
|
16
20
|
|
|
17
21
|
class LogWatcher:
|
|
18
22
|
|
|
19
|
-
def watcher(self, poll_interval=
|
|
23
|
+
def watcher(self, poll_interval=0.5):
|
|
20
24
|
self.buffer = ""
|
|
21
25
|
self.last_pos = 0
|
|
22
26
|
|
|
@@ -28,7 +32,6 @@ class LogWatcher:
|
|
|
28
32
|
self.read_log()
|
|
29
33
|
|
|
30
34
|
def read_log(self):
|
|
31
|
-
time.sleep(0.02)
|
|
32
35
|
with open(self.log_file, 'r', encoding='utf-8') as f:
|
|
33
36
|
f.seek(self.last_pos)
|
|
34
37
|
new_data = f.read()
|
|
@@ -60,15 +63,19 @@ class LogWatcher:
|
|
|
60
63
|
, flush=True)
|
|
61
64
|
|
|
62
65
|
def __init__(self, log_file):
|
|
66
|
+
logger.info(f"Watching log: {log_file}")
|
|
63
67
|
self.log_file = log_file
|
|
64
68
|
self.end_flag = False
|
|
65
69
|
|
|
66
70
|
threading.excepthook = thread_excepthook
|
|
67
|
-
t = threading.Thread(target=self.watcher, daemon=True)
|
|
68
|
-
t.start()
|
|
71
|
+
self.t = threading.Thread(target=self.watcher, daemon=True)
|
|
72
|
+
self.t.start()
|
|
69
73
|
|
|
70
74
|
def close(self):
|
|
75
|
+
logger.info("Close: LogWatcher")
|
|
71
76
|
self.end_flag = True
|
|
77
|
+
if self.t:
|
|
78
|
+
self.t.join()
|
|
72
79
|
|
|
73
80
|
|
|
74
81
|
if __name__ == "__main__":
|