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/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,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 time import sleep
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: Dict = {
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
- print("[Warning] Can not use any Driver when runing native mode.", flush=True)
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.debug(f"Log file: {LOGFILE}")
358
- logger.debug(f"Result file: {RESFILE}")
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
- print("[Warning] No property has been found.", flush=True)
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
- t = activateFastbot(options=self.options)
306
+ fb = FastbotManager(self.options, LOGFILE)
307
+ fb.start()
308
+
396
309
  log_watcher = LogWatcher(LOGFILE)
397
- if self.options.agent == "native":
398
- t.join()
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
- print("[INFO] Sending monkeyEvent {}".format(
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
- , flush=True)
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
- print(
426
- "[INFO] Exploration times up (--running-minutes)."
427
- , flush=True)
428
- end_by_remote = True
429
- break
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. Device outputDir: {res}", flush=True)
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=1):
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__":