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/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: Dict):
150
+ def logScript(self, execution_info: "PropertyExecutionInfo"):
151
151
  r = self.request(
152
152
  method="POST",
153
153
  path="/logScript",
154
- data=execution_info
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
- PropertyExecutionInfo = TypedDict(
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
- global LOGFILE, RESFILE, STAMP
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
- lastExecutedInfo: PropertyExecutionInfo = {
214
- "propName": "",
215
- "state": "",
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, outfile):
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(outfile, "w", encoding="utf-8") as fp:
242
+ with open(RESFILE, "w", encoding="utf-8") as fp:
229
243
  json.dump(json_res, fp, indent=4)
230
244
 
231
- def addExcuted(self, test: TestCase):
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
- self.lastExecutedInfo = {
234
- "propName": getFullPropName(test),
235
- "state": "start",
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["state"] = "fail"
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["state"] = "error"
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["state"] == "start":
253
- self.lastExecutedInfo["state"] = "pass"
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(outfile=RESFILE)
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 requests.ConnectionError:
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(outfile=RESFILE)
423
+ result.flushResult()
395
424
 
396
425
  if not end_by_remote:
397
426
  fb.stopMonkey()
398
- result.flushResult(outfile=RESFILE)
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("/Users/atria/Desktop/coding/Kea2/output/res_2025062510_0420056539/fastbot_2025062510_0420056539.log")
92
+ # LogWatcher()
93
+ pass