Kea2-python 0.0.1b3__py3-none-any.whl → 0.1.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/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 IO, Callable, Any, Dict, List, Literal, NewType, Union
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,11 +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 time import sleep
16
- from .adbUtils import push_file
13
+ from .bug_report_generator import BugReportGenerator
14
+ from .resultSyncer import ResultSyncer
17
15
  from .logWatcher import LogWatcher
18
16
  from .utils import TimeStamp, getProjectRoot, getLogger
19
- from .u2Driver import StaticU2UiObject
17
+ from .u2Driver import StaticU2UiObject, selector_to_xpath
18
+ from .fastbotManager import FastbotManager
20
19
  import uiautomator2 as u2
21
20
  import types
22
21
 
@@ -30,10 +29,14 @@ logger = getLogger(__name__)
30
29
  # Class Typing
31
30
  PropName = NewType("PropName", str)
32
31
  PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
32
+ PropertyExecutionInfo = TypedDict(
33
+ "PropertyExecutionInfo",
34
+ {"propName": PropName, "state": Literal["start", "pass", "fail", "error"]}
35
+ )
33
36
 
34
37
  STAMP = TimeStamp().getTimeStamp()
35
- LOGFILE = f"fastbot_{STAMP}.log"
36
- RESFILE = f"result_{STAMP}.json"
38
+ LOGFILE: str
39
+ RESFILE: str
37
40
 
38
41
  def precondition(precond: Callable[[Any], bool]) -> Callable:
39
42
  """the decorator @precondition
@@ -120,7 +123,11 @@ class Options:
120
123
  # the stamp for log file and result file, default: current time stamp
121
124
  log_stamp: str = None
122
125
  # the profiling period to get the coverage result.
123
- profile_period: int = None
126
+ profile_period: int = 25
127
+ # take screenshots for every step
128
+ take_screenshots: bool = False
129
+ # the debug mode
130
+ debug: bool = False
124
131
 
125
132
  def __setattr__(self, name, value):
126
133
  if value is None:
@@ -130,10 +137,21 @@ class Options:
130
137
  def __post_init__(self):
131
138
  if self.serial and self.Driver:
132
139
  self.Driver.setDeviceSerial(self.serial)
140
+ global LOGFILE, RESFILE, STAMP
133
141
  if self.log_stamp:
134
- global LOGFILE, RESFILE
135
- LOGFILE = f"fastbot_{self.log_stamp}.log"
136
- RESFILE = f"result_{self.log_stamp}.json"
142
+ STAMP = self.log_stamp
143
+ self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
144
+ LOGFILE = f"fastbot_{STAMP}.log"
145
+ RESFILE = f"result_{STAMP}.json"
146
+
147
+ self.profile_period = int(self.profile_period)
148
+ if self.profile_period < 1:
149
+ raise ValueError("--profile-period should be greater than 0")
150
+
151
+ self.throttle = int(self.throttle)
152
+ if self.throttle < 0:
153
+ raise ValueError("--throttle should be greater than or equal to 0")
154
+
137
155
  _check_package_installation(self.serial, self.packageNames)
138
156
 
139
157
 
@@ -168,6 +186,11 @@ def getFullPropName(testCase: TestCase):
168
186
 
169
187
  class JsonResult(TextTestResult):
170
188
  res: PBTTestResult
189
+
190
+ lastExecutedInfo: PropertyExecutionInfo = {
191
+ "propName": "",
192
+ "state": "",
193
+ }
171
194
 
172
195
  @classmethod
173
196
  def setProperties(cls, allProperties: Dict):
@@ -184,6 +207,10 @@ class JsonResult(TextTestResult):
184
207
 
185
208
  def addExcuted(self, test: TestCase):
186
209
  self.res[getFullPropName(test)].executed += 1
210
+ self.lastExecutedInfo = {
211
+ "propName": getFullPropName(test),
212
+ "state": "start",
213
+ }
187
214
 
188
215
  def addPrecondSatisfied(self, test: TestCase):
189
216
  self.res[getFullPropName(test)].precond_satisfied += 1
@@ -191,129 +218,34 @@ class JsonResult(TextTestResult):
191
218
  def addFailure(self, test, err):
192
219
  super().addFailure(test, err)
193
220
  self.res[getFullPropName(test)].fail += 1
221
+ self.lastExecutedInfo["state"] = "fail"
194
222
 
195
223
  def addError(self, test, err):
196
224
  super().addError(test, err)
197
225
  self.res[getFullPropName(test)].error += 1
226
+ self.lastExecutedInfo["state"] = "error"
227
+
228
+ def updateExectedInfo(self):
229
+ if self.lastExecutedInfo["state"] == "start":
230
+ self.lastExecutedInfo["state"] = "pass"
198
231
 
199
232
  def getExcuted(self, test: TestCase):
200
233
  return self.res[getFullPropName(test)].executed
201
234
 
202
235
 
203
- def activateFastbot(options: Options, port=None) -> threading.Thread:
204
- """
205
- activate fastbot.
206
- :params: options: the running setting for fastbot
207
- :params: port: the listening port for script driver
208
- :return: the fastbot daemon thread
209
- """
210
- cur_dir = Path(__file__).parent
211
- push_file(
212
- Path.joinpath(cur_dir, "assets/monkeyq.jar"),
213
- "/sdcard/monkeyq.jar",
214
- device=options.serial
215
- )
216
- push_file(
217
- Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
218
- "/sdcard/fastbot-thirdpart.jar",
219
- device=options.serial,
220
- )
221
- push_file(
222
- Path.joinpath(cur_dir, "assets/framework.jar"),
223
- "/sdcard/framework.jar",
224
- device=options.serial
225
- )
226
- push_file(
227
- Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a"),
228
- "/data/local/tmp",
229
- device=options.serial
230
- )
231
- push_file(
232
- Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a"),
233
- "/data/local/tmp",
234
- device=options.serial
235
- )
236
- push_file(
237
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86"),
238
- "/data/local/tmp",
239
- device=options.serial
240
- )
241
- push_file(
242
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64"),
243
- "/data/local/tmp",
244
- device=options.serial
245
- )
246
-
247
- t = startFastbotService(options)
248
- print("[INFO] Running Fastbot...", flush=True)
249
-
250
- return t
251
-
252
-
253
- def check_alive(port):
254
- """
255
- check if the script driver and proxy server are alive.
256
- """
257
- for _ in range(10):
258
- sleep(2)
259
- try:
260
- requests.get(f"http://localhost:{port}/ping")
261
- return
262
- except requests.ConnectionError:
263
- print("[INFO] waiting for connection.", flush=True)
264
- pass
265
- raise RuntimeError("Failed to connect fastbot")
266
-
267
-
268
- def startFastbotService(options: Options) -> threading.Thread:
269
- shell_command = [
270
- "CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar:/sdcard/fastbot-thirdpart.jar",
271
- "exec", "app_process",
272
- "/system/bin", "com.android.commands.monkey.Monkey",
273
- "-p", *options.packageNames,
274
- "--agent-u2" if options.agent == "u2" else "--agent",
275
- "reuseq",
276
- "--running-minutes", f"{options.running_mins}",
277
- "--throttle", f"{options.throttle}",
278
- "--bugreport", "--output-directory", "/sdcard/fastbot_report",
279
- "-v", "-v", "-v"
280
- ]
281
-
282
- full_cmd = ["adb"] + (["-s", options.serial] if options.serial else []) + ["shell"] + shell_command
283
-
284
- outfile = open(LOGFILE, "w", encoding="utf-8", buffering=1)
285
-
286
- print("[INFO] Options info: {}".format(asdict(options)), flush=True)
287
- print("[INFO] Launching fastbot with shell command:\n{}".format(" ".join(full_cmd)), flush=True)
288
- print("[INFO] Fastbot log will be saved to {}".format(outfile.name), flush=True)
289
-
290
- # process handler
291
- proc = subprocess.Popen(full_cmd, stdout=outfile, stderr=outfile)
292
- t = threading.Thread(target=close_on_exit, args=(proc, outfile), daemon=True)
293
- t.start()
294
-
295
- return t
296
-
297
-
298
- def close_on_exit(proc: subprocess.Popen, f: IO):
299
- proc.wait()
300
- f.close()
301
-
302
-
303
236
  class KeaTestRunner(TextTestRunner):
304
237
 
305
238
  resultclass: JsonResult
306
239
  allProperties: PropertyStore
307
240
  options: Options = None
308
241
  _block_funcs: Dict[Literal["widgets", "trees"], List[Callable]] = None
309
- # _block_trees_funcs = None
310
242
 
311
243
  @classmethod
312
244
  def setOptions(cls, options: Options):
313
245
  if not isinstance(options.packageNames, list) and len(options.packageNames) > 0:
314
246
  raise ValueError("packageNames should be given in a list.")
315
247
  if options.Driver is not None and options.agent == "native":
316
- print("[Warning] Can not use any Driver when runing native mode.", flush=True)
248
+ logger.warning("[Warning] Can not use any Driver when runing native mode.")
317
249
  options.Driver = None
318
250
  cls.options = options
319
251
 
@@ -332,7 +264,7 @@ class KeaTestRunner(TextTestRunner):
332
264
  self.collectAllProperties(test)
333
265
 
334
266
  if len(self.allProperties) == 0:
335
- print("[Warning] No property has been found.", flush=True)
267
+ logger.warning("[Warning] No property has been found.")
336
268
 
337
269
  self._setOuputDir()
338
270
 
@@ -361,38 +293,44 @@ class KeaTestRunner(TextTestRunner):
361
293
  message=r"Please use assert\w+ instead.",
362
294
  )
363
295
 
364
- t = activateFastbot(options=self.options)
365
296
  log_watcher = LogWatcher(LOGFILE)
366
- if self.options.agent == "native":
367
- t.join()
368
- else:
297
+ fb = FastbotManager(self.options, LOGFILE)
298
+ fb.start()
299
+
300
+ if self.options.agent == "u2":
369
301
  # initialize the result.json file
370
302
  result.flushResult(outfile=RESFILE)
371
303
  # setUp for the u2 driver
372
304
  self.scriptDriver = self.options.Driver.getScriptDriver()
373
- check_alive(port=self.scriptDriver.lport)
305
+ fb.check_alive(port=self.scriptDriver.lport)
306
+ self._init()
307
+
308
+ resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
309
+ resultSyncer.run()
374
310
 
375
311
  end_by_remote = False
376
312
  self.stepsCount = 0
377
313
  while self.stepsCount < self.options.maxStep:
378
314
 
379
315
  self.stepsCount += 1
380
- print("[INFO] Sending monkeyEvent {}".format(
316
+ logger.info("Sending monkeyEvent {}".format(
381
317
  f"({self.stepsCount} / {self.options.maxStep})" if self.options.maxStep != float("inf")
382
318
  else f"({self.stepsCount})"
383
319
  )
384
- , flush=True)
320
+ )
385
321
 
386
322
  try:
387
323
  xml_raw = self.stepMonkey()
388
- stat = self._getStat()
389
324
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
390
325
  except requests.ConnectionError:
391
- print(
392
- "[INFO] Exploration times up (--running-minutes)."
393
- , flush=True)
394
- end_by_remote = True
395
- break
326
+ if fb.get_return_code() == 0:
327
+ logger.info("[INFO] Exploration times up (--running-minutes).")
328
+ end_by_remote = True
329
+ break
330
+ raise RuntimeError("Fastbot Aborted.")
331
+
332
+ if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
333
+ resultSyncer.sync_event.set()
396
334
 
397
335
  print(f"{len(propsSatisfiedPrecond)} precond satisfied.", flush=True)
398
336
 
@@ -421,17 +359,22 @@ class KeaTestRunner(TextTestRunner):
421
359
  print("execute property %s." % execPropName, flush=True)
422
360
 
423
361
  result.addExcuted(test)
362
+ self._logScript(result.lastExecutedInfo)
424
363
  try:
425
364
  test(result)
426
365
  finally:
427
366
  result.printErrors()
428
367
 
368
+ result.updateExectedInfo()
369
+ self._logScript(result.lastExecutedInfo)
429
370
  result.flushResult(outfile=RESFILE)
430
371
 
431
372
  if not end_by_remote:
432
373
  self.stopMonkey()
433
374
  result.flushResult(outfile=RESFILE)
434
-
375
+ resultSyncer.close()
376
+
377
+ fb.join()
435
378
  print(f"Finish sending monkey events.", flush=True)
436
379
  log_watcher.close()
437
380
 
@@ -537,14 +480,31 @@ class KeaTestRunner(TextTestRunner):
537
480
  validProps[propName] = test
538
481
  return validProps
539
482
 
540
- def _getStat(self):
541
- # profile when reaching the profile period
542
- if (self.options.profile_period and
543
- self.stepsCount % self.options.profile_period == 0
544
- ):
545
- URL = f"http://localhost:{self.scriptDriver.lport}/getStat"
546
- r = requests.get(URL)
547
- res = json.loads(r.content)
483
+ def _logScript(self, execution_info:Dict):
484
+ URL = f"http://localhost:{self.scriptDriver.lport}/logScript"
485
+ r = requests.post(
486
+ url=URL,
487
+ json=execution_info
488
+ )
489
+ res = r.content.decode(encoding="utf-8")
490
+ if res != "OK":
491
+ print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
492
+
493
+ def _init(self):
494
+ URL = f"http://localhost:{self.scriptDriver.lport}/init"
495
+ data = {
496
+ "takeScreenshots": self.options.take_screenshots,
497
+ "Stamp": STAMP
498
+ }
499
+ print(f"[INFO] Init fastbot: {data}", flush=True)
500
+ r = requests.post(
501
+ url=URL,
502
+ json=data
503
+ )
504
+ res = r.content.decode(encoding="utf-8")
505
+ import re
506
+ self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
507
+ print(f"[INFO] Fastbot initiated. Device outputDir: {res}", flush=True)
548
508
 
549
509
  def collectAllProperties(self, test: TestSuite):
550
510
  """collect all the properties to prepare for PBT
@@ -560,8 +520,8 @@ class KeaTestRunner(TextTestRunner):
560
520
  """remove the tearDown function in PBT
561
521
  """
562
522
  def tearDown(self): ...
563
- testCase = types.MethodType(tearDown, testCase)
564
-
523
+ testCase.tearDown = types.MethodType(tearDown, testCase)
524
+
565
525
  def iter_tests(suite):
566
526
  for test in suite:
567
527
  if isinstance(test, TestSuite):
@@ -654,8 +614,8 @@ class KeaTestRunner(TextTestRunner):
654
614
  _widgets = _widgets if isinstance(_widgets, list) else [_widgets]
655
615
  for w in _widgets:
656
616
  if isinstance(w, StaticU2UiObject):
657
- xpath = w._getXPath(w.selector)
658
- blocked_set.add(xpath) # 集合去重
617
+ xpath = selector_to_xpath(w.selector, True)
618
+ blocked_set.add(xpath)
659
619
  elif isinstance(w, u2.xpath.XPathSelector):
660
620
  xpath = w._parent.xpath
661
621
  blocked_set.add(xpath)
@@ -691,5 +651,15 @@ class KeaTestRunner(TextTestRunner):
691
651
  def __del__(self):
692
652
  """tearDown method. Cleanup the env.
693
653
  """
654
+ try:
655
+ logger.debug("Generating test bug report")
656
+ report_generator = BugReportGenerator(self.options.output_dir)
657
+ report_generator.generate_report()
658
+ except Exception as e:
659
+ logger.error(f"Error generating bug report: {e}", flush=True)
660
+ try:
661
+ self.stopMonkey()
662
+ except Exception as e:
663
+ pass
694
664
  if self.options.Driver:
695
665
  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 True:
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)