Kea2-python 0.2.2__py3-none-any.whl → 0.2.4__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/adbUtils.py +3 -1
- kea2/assets/monkeyq.jar +0 -0
- kea2/bug_report_generator.py +570 -233
- kea2/cli.py +42 -0
- kea2/fastbotManager.py +8 -3
- kea2/keaUtils.py +58 -53
- kea2/kea_launcher.py +2 -0
- kea2/logWatcher.py +16 -2
- kea2/templates/bug_report_template.html +925 -171
- kea2/u2Driver.py +2 -1
- kea2/utils.py +14 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/METADATA +4 -5
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/RECORD +17 -17
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/WHEEL +0 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/entry_points.txt +0 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-0.2.2.dist-info → kea2_python-0.2.4.dist-info}/top_level.txt +0 -0
kea2/cli.py
CHANGED
|
@@ -46,6 +46,34 @@ def cmd_load_configs(args):
|
|
|
46
46
|
pass
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def cmd_report(args):
|
|
50
|
+
from .bug_report_generator import BugReportGenerator
|
|
51
|
+
try:
|
|
52
|
+
report_dir = args.path
|
|
53
|
+
if not report_dir:
|
|
54
|
+
logger.error("Report directory path is required. Use -p to specify the path.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
report_path = Path(report_dir)
|
|
58
|
+
if not report_path.exists():
|
|
59
|
+
logger.error(f"Report directory does not exist: {report_dir}")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
logger.debug(f"Generating test report from directory: {report_dir}")
|
|
63
|
+
|
|
64
|
+
generator = BugReportGenerator()
|
|
65
|
+
report_file = generator.generate_report(report_path)
|
|
66
|
+
|
|
67
|
+
if report_file:
|
|
68
|
+
logger.debug(f"Test report generated successfully: {report_file}")
|
|
69
|
+
print(f"Report saved to: {report_file}", flush=True)
|
|
70
|
+
else:
|
|
71
|
+
logger.error("Failed to generate test report")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error generating test report: {e}")
|
|
75
|
+
|
|
76
|
+
|
|
49
77
|
def cmd_run(args):
|
|
50
78
|
base_dir = getProjectRoot()
|
|
51
79
|
if base_dir is None:
|
|
@@ -60,6 +88,20 @@ _commands = [
|
|
|
60
88
|
action=cmd_init,
|
|
61
89
|
command="init",
|
|
62
90
|
help="init the Kea2 project in current directory",
|
|
91
|
+
),
|
|
92
|
+
dict(
|
|
93
|
+
action=cmd_report,
|
|
94
|
+
command="report",
|
|
95
|
+
help="generate test report from existing test results",
|
|
96
|
+
flags=[
|
|
97
|
+
dict(
|
|
98
|
+
name=["report_dir"],
|
|
99
|
+
args=["-p", "--path"],
|
|
100
|
+
type=str,
|
|
101
|
+
required=True,
|
|
102
|
+
help="Path to the directory containing test results"
|
|
103
|
+
)
|
|
104
|
+
]
|
|
63
105
|
)
|
|
64
106
|
]
|
|
65
107
|
|
kea2/fastbotManager.py
CHANGED
|
@@ -13,7 +13,7 @@ from kea2.utils import getLogger
|
|
|
13
13
|
|
|
14
14
|
from typing import IO, TYPE_CHECKING, Dict
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
|
-
from .keaUtils import Options
|
|
16
|
+
from .keaUtils import Options, PropertyExecutionInfo
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
logger = getLogger(__name__)
|
|
@@ -147,11 +147,15 @@ class FastbotManager:
|
|
|
147
147
|
print(f"[Server INFO] {r.text}", flush=True)
|
|
148
148
|
|
|
149
149
|
@retry(Exception, tries=2, delay=2)
|
|
150
|
-
def logScript(self, execution_info:
|
|
150
|
+
def logScript(self, execution_info: "PropertyExecutionInfo"):
|
|
151
151
|
r = self.request(
|
|
152
152
|
method="POST",
|
|
153
153
|
path="/logScript",
|
|
154
|
-
data=
|
|
154
|
+
data={
|
|
155
|
+
"propName": execution_info.propName,
|
|
156
|
+
"startStepsCount": execution_info.startStepsCount,
|
|
157
|
+
"state": execution_info.state,
|
|
158
|
+
}
|
|
155
159
|
)
|
|
156
160
|
res = r.text
|
|
157
161
|
if res != "OK":
|
|
@@ -177,6 +181,7 @@ class FastbotManager:
|
|
|
177
181
|
"--running-minutes", f"{self.options.running_mins}",
|
|
178
182
|
"--throttle", f"{self.options.throttle}",
|
|
179
183
|
"--bugreport",
|
|
184
|
+
"--output-directory", f"{self.options.device_output_root}/output_{self.options.log_stamp}",
|
|
180
185
|
]
|
|
181
186
|
|
|
182
187
|
if self.options.profile_period:
|
kea2/keaUtils.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
from collections import deque
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
import traceback
|
|
5
6
|
import time
|
|
6
|
-
from typing import Callable, Any, Dict, List, Literal, NewType, TypedDict, Union
|
|
7
|
+
from typing import Callable, Any, Deque, Dict, List, Literal, NewType, TypedDict, Union
|
|
7
8
|
from unittest import TextTestRunner, registerResult, TestSuite, TestCase, TextTestResult
|
|
8
9
|
import random
|
|
9
10
|
import warnings
|
|
10
11
|
from dataclasses import dataclass, asdict
|
|
11
|
-
import requests
|
|
12
12
|
from kea2.absDriver import AbstractDriver
|
|
13
13
|
from functools import wraps
|
|
14
14
|
from kea2.bug_report_generator import BugReportGenerator
|
|
@@ -31,14 +31,12 @@ logger = getLogger(__name__)
|
|
|
31
31
|
# Class Typing
|
|
32
32
|
PropName = NewType("PropName", str)
|
|
33
33
|
PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
|
|
34
|
-
|
|
35
|
-
"PropertyExecutionInfo",
|
|
36
|
-
{"propName": PropName, "state": Literal["start", "pass", "fail", "error"]}
|
|
37
|
-
)
|
|
34
|
+
|
|
38
35
|
|
|
39
36
|
STAMP = TimeStamp().getTimeStamp()
|
|
40
37
|
LOGFILE: str
|
|
41
38
|
RESFILE: str
|
|
39
|
+
PROP_EXEC_RESFILE: str
|
|
42
40
|
|
|
43
41
|
def precondition(precond: Callable[[Any], bool]) -> Callable:
|
|
44
42
|
"""the decorator @precondition
|
|
@@ -147,6 +145,7 @@ class Options:
|
|
|
147
145
|
def __post_init__(self):
|
|
148
146
|
import logging
|
|
149
147
|
logging.basicConfig(level=logging.DEBUG if self.debug else logging.INFO)
|
|
148
|
+
|
|
150
149
|
if self.Driver:
|
|
151
150
|
target_device = dict()
|
|
152
151
|
if self.serial:
|
|
@@ -155,7 +154,8 @@ class Options:
|
|
|
155
154
|
target_device["transport_id"] = self.transport_id
|
|
156
155
|
self.Driver.setDevice(target_device)
|
|
157
156
|
ADBDevice.setDevice(self.serial, self.transport_id)
|
|
158
|
-
|
|
157
|
+
|
|
158
|
+
global LOGFILE, RESFILE, PROP_EXEC_RESFILE, STAMP
|
|
159
159
|
if self.log_stamp:
|
|
160
160
|
illegal_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\n', '\r', '\t', '\0']
|
|
161
161
|
for char in illegal_chars:
|
|
@@ -164,9 +164,13 @@ class Options:
|
|
|
164
164
|
f"char: `{char}` is illegal in --log-stamp. current stamp: {self.log_stamp}"
|
|
165
165
|
)
|
|
166
166
|
STAMP = self.log_stamp
|
|
167
|
+
|
|
168
|
+
self.log_stamp = STAMP
|
|
169
|
+
|
|
167
170
|
self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
|
|
168
171
|
LOGFILE = f"fastbot_{STAMP}.log"
|
|
169
172
|
RESFILE = f"result_{STAMP}.json"
|
|
173
|
+
PROP_EXEC_RESFILE = f"property_exec_info_{STAMP}.json"
|
|
170
174
|
|
|
171
175
|
self.profile_period = int(self.profile_period)
|
|
172
176
|
if self.profile_period < 1:
|
|
@@ -195,6 +199,16 @@ class PropStatistic:
|
|
|
195
199
|
fail: int = 0
|
|
196
200
|
error: int = 0
|
|
197
201
|
|
|
202
|
+
|
|
203
|
+
PropertyExecutionInfoStore = NewType("PropertyExecutionInfoStore", Deque["PropertyExecutionInfo"])
|
|
204
|
+
@dataclass
|
|
205
|
+
class PropertyExecutionInfo:
|
|
206
|
+
startStepsCount: int
|
|
207
|
+
propName: PropName
|
|
208
|
+
state: Literal["start", "pass", "fail", "error"]
|
|
209
|
+
tb: str
|
|
210
|
+
|
|
211
|
+
|
|
198
212
|
class PBTTestResult(dict):
|
|
199
213
|
def __getitem__(self, key) -> PropStatistic:
|
|
200
214
|
return super().__getitem__(key)
|
|
@@ -207,13 +221,12 @@ def getFullPropName(testCase: TestCase):
|
|
|
207
221
|
testCase._testMethodName
|
|
208
222
|
])
|
|
209
223
|
|
|
224
|
+
|
|
210
225
|
class JsonResult(TextTestResult):
|
|
211
|
-
res: PBTTestResult
|
|
212
226
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
227
|
+
res: PBTTestResult
|
|
228
|
+
lastExecutedInfo: PropertyExecutionInfo
|
|
229
|
+
executionInfoStore: PropertyExecutionInfoStore = deque()
|
|
217
230
|
|
|
218
231
|
@classmethod
|
|
219
232
|
def setProperties(cls, allProperties: Dict):
|
|
@@ -221,19 +234,28 @@ class JsonResult(TextTestResult):
|
|
|
221
234
|
for testCase in allProperties.values():
|
|
222
235
|
cls.res[getFullPropName(testCase)] = PropStatistic()
|
|
223
236
|
|
|
224
|
-
def flushResult(self
|
|
237
|
+
def flushResult(self):
|
|
238
|
+
global RESFILE, PROP_EXEC_RESFILE
|
|
225
239
|
json_res = dict()
|
|
226
240
|
for propName, propStatitic in self.res.items():
|
|
227
241
|
json_res[propName] = asdict(propStatitic)
|
|
228
|
-
with open(
|
|
242
|
+
with open(RESFILE, "w", encoding="utf-8") as fp:
|
|
229
243
|
json.dump(json_res, fp, indent=4)
|
|
230
244
|
|
|
231
|
-
|
|
245
|
+
while self.executionInfoStore:
|
|
246
|
+
execInfo = self.executionInfoStore.popleft()
|
|
247
|
+
with open(PROP_EXEC_RESFILE, "a", encoding="utf-8") as fp:
|
|
248
|
+
fp.write(f"{json.dumps(asdict(execInfo))}\n")
|
|
249
|
+
|
|
250
|
+
def addExcuted(self, test: TestCase, stepsCount: int):
|
|
232
251
|
self.res[getFullPropName(test)].executed += 1
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
|
|
253
|
+
self.lastExecutedInfo = PropertyExecutionInfo(
|
|
254
|
+
propName=getFullPropName(test),
|
|
255
|
+
state="start",
|
|
256
|
+
tb="",
|
|
257
|
+
startStepsCount=stepsCount
|
|
258
|
+
)
|
|
237
259
|
|
|
238
260
|
def addPrecondSatisfied(self, test: TestCase):
|
|
239
261
|
self.res[getFullPropName(test)].precond_satisfied += 1
|
|
@@ -241,16 +263,21 @@ class JsonResult(TextTestResult):
|
|
|
241
263
|
def addFailure(self, test, err):
|
|
242
264
|
super().addFailure(test, err)
|
|
243
265
|
self.res[getFullPropName(test)].fail += 1
|
|
244
|
-
self.lastExecutedInfo
|
|
266
|
+
self.lastExecutedInfo.state = "fail"
|
|
267
|
+
self.lastExecutedInfo.tb = self._exc_info_to_string(err, test)
|
|
245
268
|
|
|
246
269
|
def addError(self, test, err):
|
|
247
270
|
super().addError(test, err)
|
|
248
271
|
self.res[getFullPropName(test)].error += 1
|
|
249
|
-
self.lastExecutedInfo
|
|
272
|
+
self.lastExecutedInfo.state = "error"
|
|
273
|
+
self.lastExecutedInfo.tb = self._exc_info_to_string(err, test)
|
|
250
274
|
|
|
251
275
|
def updateExectedInfo(self):
|
|
252
|
-
if self.lastExecutedInfo
|
|
253
|
-
self.lastExecutedInfo
|
|
276
|
+
if self.lastExecutedInfo.state == "start":
|
|
277
|
+
self.lastExecutedInfo.state = "pass"
|
|
278
|
+
|
|
279
|
+
self.executionInfoStore.append(self.lastExecutedInfo)
|
|
280
|
+
|
|
254
281
|
|
|
255
282
|
def getExcuted(self, test: TestCase):
|
|
256
283
|
return self.res[getFullPropName(test)].executed
|
|
@@ -275,11 +302,13 @@ class KeaTestRunner(TextTestRunner):
|
|
|
275
302
|
def _setOuputDir(self):
|
|
276
303
|
output_dir = Path(self.options.output_dir).absolute()
|
|
277
304
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
278
|
-
global LOGFILE, RESFILE
|
|
305
|
+
global LOGFILE, RESFILE, PROP_EXEC_RESFILE
|
|
279
306
|
LOGFILE = output_dir / Path(LOGFILE)
|
|
280
307
|
RESFILE = output_dir / Path(RESFILE)
|
|
308
|
+
PROP_EXEC_RESFILE = output_dir / Path(PROP_EXEC_RESFILE)
|
|
281
309
|
logger.info(f"Log file: {LOGFILE}")
|
|
282
310
|
logger.info(f"Result file: {RESFILE}")
|
|
311
|
+
logger.info(f"Property execution info file: {PROP_EXEC_RESFILE}")
|
|
283
312
|
|
|
284
313
|
def run(self, test):
|
|
285
314
|
|
|
@@ -323,7 +352,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
323
352
|
|
|
324
353
|
if self.options.agent == "u2":
|
|
325
354
|
# initialize the result.json file
|
|
326
|
-
result.flushResult(
|
|
355
|
+
result.flushResult()
|
|
327
356
|
# setUp for the u2 driver
|
|
328
357
|
self.scriptDriver = self.options.Driver.getScriptDriver()
|
|
329
358
|
fb.check_alive()
|
|
@@ -347,7 +376,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
347
376
|
try:
|
|
348
377
|
xml_raw = fb.stepMonkey(self._monkeyStepInfo)
|
|
349
378
|
propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
|
|
350
|
-
except
|
|
379
|
+
except u2.HTTPError:
|
|
351
380
|
logger.info("Connection refused by remote.")
|
|
352
381
|
if fb.get_return_code() == 0:
|
|
353
382
|
logger.info("Exploration times up (--running-minutes).")
|
|
@@ -382,7 +411,7 @@ class KeaTestRunner(TextTestRunner):
|
|
|
382
411
|
setattr(test, self.options.driverName, self.scriptDriver)
|
|
383
412
|
print("execute property %s." % execPropName, flush=True)
|
|
384
413
|
|
|
385
|
-
result.addExcuted(test)
|
|
414
|
+
result.addExcuted(test, self.stepsCount)
|
|
386
415
|
fb.logScript(result.lastExecutedInfo)
|
|
387
416
|
try:
|
|
388
417
|
test(result)
|
|
@@ -391,11 +420,11 @@ class KeaTestRunner(TextTestRunner):
|
|
|
391
420
|
|
|
392
421
|
result.updateExectedInfo()
|
|
393
422
|
fb.logScript(result.lastExecutedInfo)
|
|
394
|
-
result.flushResult(
|
|
423
|
+
result.flushResult()
|
|
395
424
|
|
|
396
425
|
if not end_by_remote:
|
|
397
426
|
fb.stopMonkey()
|
|
398
|
-
result.flushResult(
|
|
427
|
+
result.flushResult()
|
|
399
428
|
resultSyncer.close()
|
|
400
429
|
|
|
401
430
|
fb.join()
|
|
@@ -438,30 +467,6 @@ class KeaTestRunner(TextTestRunner):
|
|
|
438
467
|
self.stream.flush()
|
|
439
468
|
return result
|
|
440
469
|
|
|
441
|
-
# def stepMonkey(self) -> str:
|
|
442
|
-
# """
|
|
443
|
-
# send a step monkey request to the server and get the xml string.
|
|
444
|
-
# """
|
|
445
|
-
# block_dict = self._getBlockedWidgets()
|
|
446
|
-
# block_widgets: List[str] = block_dict['widgets']
|
|
447
|
-
# block_trees: List[str] = block_dict['trees']
|
|
448
|
-
# URL = f"http://localhost:{self.scriptDriver.lport}/stepMonkey"
|
|
449
|
-
# logger.debug(f"Sending request: {URL}")
|
|
450
|
-
# logger.debug(f"Blocking widgets: {block_widgets}")
|
|
451
|
-
# logger.debug(f"Blocking trees: {block_trees}")
|
|
452
|
-
# r = requests.post(
|
|
453
|
-
# url=URL,
|
|
454
|
-
# json={
|
|
455
|
-
# "steps_count": self.stepsCount,
|
|
456
|
-
# "block_widgets": block_widgets,
|
|
457
|
-
# "block_trees": block_trees
|
|
458
|
-
# }
|
|
459
|
-
# )
|
|
460
|
-
|
|
461
|
-
# res = json.loads(r.content)
|
|
462
|
-
# xml_raw = res["result"]
|
|
463
|
-
# return xml_raw
|
|
464
|
-
|
|
465
470
|
@property
|
|
466
471
|
def _monkeyStepInfo(self):
|
|
467
472
|
r = self._get_block_widgets()
|
kea2/kea_launcher.py
CHANGED
|
@@ -170,6 +170,8 @@ def driver_info_logger(args):
|
|
|
170
170
|
print(" log_stamp:", args.log_stamp, flush=True)
|
|
171
171
|
if args.take_screenshots:
|
|
172
172
|
print(" take_screenshots:", args.take_screenshots, flush=True)
|
|
173
|
+
if args.max_step:
|
|
174
|
+
print(" max_step:", args.max_step, flush=True)
|
|
173
175
|
|
|
174
176
|
|
|
175
177
|
def parse_args(argv: List):
|
kea2/logWatcher.py
CHANGED
|
@@ -51,9 +51,10 @@ class LogWatcher:
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
statistic_match = PATTERN_STATISTIC.search(content)
|
|
54
|
-
if statistic_match:
|
|
54
|
+
if statistic_match and not self.statistic_printed:
|
|
55
55
|
statistic_body = statistic_match.group(1).strip()
|
|
56
56
|
if statistic_body:
|
|
57
|
+
self.statistic_printed = True
|
|
57
58
|
print(
|
|
58
59
|
"[INFO] Fastbot exit:\n" +
|
|
59
60
|
statistic_body
|
|
@@ -63,6 +64,7 @@ class LogWatcher:
|
|
|
63
64
|
logger.info(f"Watching log: {log_file}")
|
|
64
65
|
self.log_file = log_file
|
|
65
66
|
self.end_flag = False
|
|
67
|
+
self.statistic_printed = False
|
|
66
68
|
|
|
67
69
|
threading.excepthook = thread_excepthook
|
|
68
70
|
self.t = threading.Thread(target=self.watcher, daemon=True)
|
|
@@ -73,7 +75,19 @@ class LogWatcher:
|
|
|
73
75
|
self.end_flag = True
|
|
74
76
|
if self.t:
|
|
75
77
|
self.t.join()
|
|
78
|
+
|
|
79
|
+
if not self.statistic_printed:
|
|
80
|
+
self._parse_whole_log()
|
|
81
|
+
|
|
82
|
+
def _parse_whole_log(self):
|
|
83
|
+
logger.warning(
|
|
84
|
+
"LogWatcher closed without reading the statistics, parsing the whole log now."
|
|
85
|
+
)
|
|
86
|
+
with open(self.log_file, "r", encoding="utf-8") as fp:
|
|
87
|
+
content = fp.read()
|
|
88
|
+
self.parse_log(content)
|
|
76
89
|
|
|
77
90
|
|
|
78
91
|
if __name__ == "__main__":
|
|
79
|
-
LogWatcher(
|
|
92
|
+
# LogWatcher()
|
|
93
|
+
pass
|