Kea2-python 0.0.1b3__py3-none-any.whl → 0.1.0b0__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/__init__.py CHANGED
@@ -1,4 +1 @@
1
- from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options
2
-
3
- import logging
4
- logging.basicConfig(level=logging.DEBUG)
1
+ from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options
kea2/adbUtils.py CHANGED
@@ -147,7 +147,6 @@ def pull_file(remote_path: str, local_path: str, device: Optional[str] = None):
147
147
  """
148
148
  return run_adb_command(["-s", device, "pull", remote_path, local_path])
149
149
 
150
-
151
150
  # Forward-related functions
152
151
 
153
152
 
kea2/assets/monkeyq.jar CHANGED
Binary file
kea2/keaUtils.py CHANGED
@@ -14,9 +14,10 @@ from .absDriver import AbstractDriver
14
14
  from functools import wraps
15
15
  from time import sleep
16
16
  from .adbUtils import push_file
17
+ from .resultSyncer import ResultSyncer
17
18
  from .logWatcher import LogWatcher
18
19
  from .utils import TimeStamp, getProjectRoot, getLogger
19
- from .u2Driver import StaticU2UiObject
20
+ from .u2Driver import StaticU2UiObject, selector_to_xpath
20
21
  import uiautomator2 as u2
21
22
  import types
22
23
 
@@ -32,8 +33,8 @@ PropName = NewType("PropName", str)
32
33
  PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
33
34
 
34
35
  STAMP = TimeStamp().getTimeStamp()
35
- LOGFILE = f"fastbot_{STAMP}.log"
36
- RESFILE = f"result_{STAMP}.json"
36
+ LOGFILE: str
37
+ RESFILE: str
37
38
 
38
39
  def precondition(precond: Callable[[Any], bool]) -> Callable:
39
40
  """the decorator @precondition
@@ -120,7 +121,11 @@ class Options:
120
121
  # the stamp for log file and result file, default: current time stamp
121
122
  log_stamp: str = None
122
123
  # the profiling period to get the coverage result.
123
- profile_period: int = None
124
+ profile_period: int = 25
125
+ # take screenshots for every step
126
+ take_screenshots: bool = False
127
+ # the debug mode
128
+ debug: bool = False
124
129
 
125
130
  def __setattr__(self, name, value):
126
131
  if value is None:
@@ -130,10 +135,21 @@ class Options:
130
135
  def __post_init__(self):
131
136
  if self.serial and self.Driver:
132
137
  self.Driver.setDeviceSerial(self.serial)
138
+ global LOGFILE, RESFILE, STAMP
133
139
  if self.log_stamp:
134
- global LOGFILE, RESFILE
135
- LOGFILE = f"fastbot_{self.log_stamp}.log"
136
- RESFILE = f"result_{self.log_stamp}.json"
140
+ STAMP = self.log_stamp
141
+ self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
142
+ LOGFILE = f"fastbot_{STAMP}.log"
143
+ RESFILE = f"result_{STAMP}.json"
144
+
145
+ self.profile_period = int(self.profile_period)
146
+ if self.profile_period < 1:
147
+ raise ValueError("--profile-period should be greater than 0")
148
+
149
+ self.throttle = int(self.throttle)
150
+ if self.throttle < 0:
151
+ raise ValueError("--throttle should be greater than or equal to 0")
152
+
137
153
  _check_package_installation(self.serial, self.packageNames)
138
154
 
139
155
 
@@ -168,6 +184,11 @@ def getFullPropName(testCase: TestCase):
168
184
 
169
185
  class JsonResult(TextTestResult):
170
186
  res: PBTTestResult
187
+
188
+ lastExecutedInfo: Dict = {
189
+ "propName": "",
190
+ "state": "",
191
+ }
171
192
 
172
193
  @classmethod
173
194
  def setProperties(cls, allProperties: Dict):
@@ -184,6 +205,10 @@ class JsonResult(TextTestResult):
184
205
 
185
206
  def addExcuted(self, test: TestCase):
186
207
  self.res[getFullPropName(test)].executed += 1
208
+ self.lastExecutedInfo = {
209
+ "propName": getFullPropName(test),
210
+ "state": "start",
211
+ }
187
212
 
188
213
  def addPrecondSatisfied(self, test: TestCase):
189
214
  self.res[getFullPropName(test)].precond_satisfied += 1
@@ -191,10 +216,12 @@ class JsonResult(TextTestResult):
191
216
  def addFailure(self, test, err):
192
217
  super().addFailure(test, err)
193
218
  self.res[getFullPropName(test)].fail += 1
219
+ self.lastExecutedInfo["state"] = "fail"
194
220
 
195
221
  def addError(self, test, err):
196
222
  super().addError(test, err)
197
223
  self.res[getFullPropName(test)].error += 1
224
+ self.lastExecutedInfo["state"] = "error"
198
225
 
199
226
  def getExcuted(self, test: TestCase):
200
227
  return self.res[getFullPropName(test)].executed
@@ -271,14 +298,18 @@ def startFastbotService(options: Options) -> threading.Thread:
271
298
  "exec", "app_process",
272
299
  "/system/bin", "com.android.commands.monkey.Monkey",
273
300
  "-p", *options.packageNames,
274
- "--agent-u2" if options.agent == "u2" else "--agent",
301
+ "--agent-u2" if options.agent == "u2" else "--agent",
275
302
  "reuseq",
276
303
  "--running-minutes", f"{options.running_mins}",
277
304
  "--throttle", f"{options.throttle}",
278
- "--bugreport", "--output-directory", "/sdcard/fastbot_report",
279
- "-v", "-v", "-v"
305
+ "--bugreport",
280
306
  ]
281
307
 
308
+ if options.profile_period:
309
+ shell_command += ["--profile-period", f"{options.profile_period}"]
310
+
311
+ shell_command += ["-v", "-v", "-v"]
312
+
282
313
  full_cmd = ["adb"] + (["-s", options.serial] if options.serial else []) + ["shell"] + shell_command
283
314
 
284
315
  outfile = open(LOGFILE, "w", encoding="utf-8", buffering=1)
@@ -298,7 +329,7 @@ def startFastbotService(options: Options) -> threading.Thread:
298
329
  def close_on_exit(proc: subprocess.Popen, f: IO):
299
330
  proc.wait()
300
331
  f.close()
301
-
332
+
302
333
 
303
334
  class KeaTestRunner(TextTestRunner):
304
335
 
@@ -371,6 +402,10 @@ class KeaTestRunner(TextTestRunner):
371
402
  # setUp for the u2 driver
372
403
  self.scriptDriver = self.options.Driver.getScriptDriver()
373
404
  check_alive(port=self.scriptDriver.lport)
405
+ self._init()
406
+
407
+ resultSyncer = ResultSyncer(self.device_output_dir, self.options.output_dir)
408
+ resultSyncer.run()
374
409
 
375
410
  end_by_remote = False
376
411
  self.stepsCount = 0
@@ -385,7 +420,6 @@ class KeaTestRunner(TextTestRunner):
385
420
 
386
421
  try:
387
422
  xml_raw = self.stepMonkey()
388
- stat = self._getStat()
389
423
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
390
424
  except requests.ConnectionError:
391
425
  print(
@@ -394,6 +428,9 @@ class KeaTestRunner(TextTestRunner):
394
428
  end_by_remote = True
395
429
  break
396
430
 
431
+ if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
432
+ resultSyncer.sync_event.set()
433
+
397
434
  print(f"{len(propsSatisfiedPrecond)} precond satisfied.", flush=True)
398
435
 
399
436
  # Go to the next round if no precond satisfied
@@ -421,19 +458,23 @@ class KeaTestRunner(TextTestRunner):
421
458
  print("execute property %s." % execPropName, flush=True)
422
459
 
423
460
  result.addExcuted(test)
461
+ self._logScript(result.lastExecutedInfo)
424
462
  try:
425
463
  test(result)
426
464
  finally:
427
465
  result.printErrors()
428
466
 
467
+ self._logScript(result.lastExecutedInfo)
429
468
  result.flushResult(outfile=RESFILE)
430
469
 
431
470
  if not end_by_remote:
432
471
  self.stopMonkey()
433
472
  result.flushResult(outfile=RESFILE)
473
+ resultSyncer.close()
434
474
 
435
475
  print(f"Finish sending monkey events.", flush=True)
436
476
  log_watcher.close()
477
+
437
478
 
438
479
  # Source code from unittest Runner
439
480
  # process the result
@@ -537,14 +578,31 @@ class KeaTestRunner(TextTestRunner):
537
578
  validProps[propName] = test
538
579
  return validProps
539
580
 
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)
581
+ def _logScript(self, execution_info:Dict):
582
+ URL = f"http://localhost:{self.scriptDriver.lport}/logScript"
583
+ r = requests.post(
584
+ url=URL,
585
+ json=execution_info
586
+ )
587
+ res = r.content.decode(encoding="utf-8")
588
+ if res != "OK":
589
+ print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
590
+
591
+ def _init(self):
592
+ URL = f"http://localhost:{self.scriptDriver.lport}/init"
593
+ data = {
594
+ "takeScreenshots": self.options.take_screenshots,
595
+ "Stamp": STAMP
596
+ }
597
+ print(f"[INFO] Init fastbot: {data}", flush=True)
598
+ r = requests.post(
599
+ url=URL,
600
+ json=data
601
+ )
602
+ res = r.content.decode(encoding="utf-8")
603
+ import re
604
+ self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
605
+ print(f"[INFO] Fastbot initiated. Device outputDir: {res}", flush=True)
548
606
 
549
607
  def collectAllProperties(self, test: TestSuite):
550
608
  """collect all the properties to prepare for PBT
@@ -560,7 +618,7 @@ class KeaTestRunner(TextTestRunner):
560
618
  """remove the tearDown function in PBT
561
619
  """
562
620
  def tearDown(self): ...
563
- testCase = types.MethodType(tearDown, testCase)
621
+ testCase.tearDown = types.MethodType(tearDown, testCase)
564
622
 
565
623
  def iter_tests(suite):
566
624
  for test in suite:
@@ -654,8 +712,8 @@ class KeaTestRunner(TextTestRunner):
654
712
  _widgets = _widgets if isinstance(_widgets, list) else [_widgets]
655
713
  for w in _widgets:
656
714
  if isinstance(w, StaticU2UiObject):
657
- xpath = w._getXPath(w.selector)
658
- blocked_set.add(xpath) # 集合去重
715
+ xpath = selector_to_xpath(w.selector, True)
716
+ blocked_set.add(xpath)
659
717
  elif isinstance(w, u2.xpath.XPathSelector):
660
718
  xpath = w._parent.xpath
661
719
  blocked_set.add(xpath)
@@ -691,5 +749,9 @@ class KeaTestRunner(TextTestRunner):
691
749
  def __del__(self):
692
750
  """tearDown method. Cleanup the env.
693
751
  """
752
+ try:
753
+ self.stopMonkey()
754
+ except Exception as e:
755
+ pass
694
756
  if self.options.Driver:
695
757
  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)
kea2/u2Driver.py CHANGED
@@ -123,6 +123,7 @@ class StaticU2UiObject(u2.UiObject):
123
123
  xpath = f".//node{''.join(attrLocs)}"
124
124
  return xpath
125
125
 
126
+
126
127
  @property
127
128
  def exists(self):
128
129
  dict.update(self.selector, {"covered": "false"})
@@ -134,6 +135,12 @@ class StaticU2UiObject(u2.UiObject):
134
135
  xpath = self._getXPath(self.selector)
135
136
  matched_widgets = self.session.xml.xpath(xpath)
136
137
  return len(matched_widgets)
138
+
139
+ def child(self, **kwargs):
140
+ return StaticU2UiObject(self.session, self.selector.clone().child(**kwargs))
141
+
142
+ def sibling(self, **kwargs):
143
+ return StaticU2UiObject(self.session, self.selector.clone().sibling(**kwargs))
137
144
 
138
145
 
139
146
  def _get_bounds(raw_bounds):
@@ -324,6 +331,89 @@ def forward_port(self, remote: Union[int, str]) -> int:
324
331
  return local_port
325
332
 
326
333
 
334
+ def selector_to_xpath(selector: u2.Selector, is_initial: bool = True) -> str:
335
+ """
336
+ Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
337
+
338
+ Args:
339
+ selector (u2.Selector): A u2 Selector object
340
+ is_initial (bool): Whether it is the initial node, defaults to True
341
+
342
+ Returns:
343
+ str: The corresponding XPath expression
344
+ """
345
+ try:
346
+ if is_initial:
347
+ xpath = ".//node"
348
+ else:
349
+ xpath = "node"
350
+
351
+ conditions = []
352
+
353
+ if "className" in selector:
354
+ conditions.insert(0, f"[@class='{selector['className']}']") # 将 className 条件放在前面
355
+
356
+ if "text" in selector:
357
+ conditions.append(f"[@text='{selector['text']}']")
358
+ elif "textContains" in selector:
359
+ conditions.append(f"[contains(@text, '{selector['textContains']}')]")
360
+ elif "textMatches" in selector:
361
+ conditions.append(f"[re:match(@text, '{selector['textMatches']}')]")
362
+ elif "textStartsWith" in selector:
363
+ conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
364
+
365
+ if "description" in selector:
366
+ conditions.append(f"[@content-desc='{selector['description']}']")
367
+ elif "descriptionContains" in selector:
368
+ conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
369
+ elif "descriptionMatches" in selector:
370
+ conditions.append(f"[re:match(@content-desc, '{selector['descriptionMatches']}')]")
371
+ elif "descriptionStartsWith" in selector:
372
+ conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
373
+
374
+ if "packageName" in selector:
375
+ conditions.append(f"[@package='{selector['packageName']}']")
376
+ elif "packageNameMatches" in selector:
377
+ conditions.append(f"[re:match(@package, '{selector['packageNameMatches']}')]")
378
+
379
+ if "resourceId" in selector:
380
+ conditions.append(f"[@resource-id='{selector['resourceId']}']")
381
+ elif "resourceIdMatches" in selector:
382
+ conditions.append(f"[re:match(@resource-id, '{selector['resourceIdMatches']}')]")
383
+
384
+ bool_props = [
385
+ "checkable", "checked", "clickable", "longClickable", "scrollable",
386
+ "enabled", "focusable", "focused", "selected", "covered"
387
+ ]
388
+ for prop in bool_props:
389
+ if prop in selector:
390
+ value = "true" if selector[prop] else "false"
391
+ conditions.append(f"[@{prop}='{value}']")
392
+
393
+ if "index" in selector:
394
+ conditions.append(f"[@index='{selector['index']}']")
395
+ elif "instance" in selector:
396
+ conditions.append(f"[@instance='{selector['instance']}']")
397
+
398
+ xpath += "".join(conditions)
399
+
400
+ if "childOrSibling" in selector and selector["childOrSibling"]:
401
+ for i, relation in enumerate(selector["childOrSibling"]):
402
+ sub_selector = selector["childOrSiblingSelector"][i]
403
+ sub_xpath = selector_to_xpath(sub_selector, False) # 递归处理子选择器
404
+
405
+ if relation == "child":
406
+ xpath += f"/{sub_xpath}"
407
+ elif relation == "sibling":
408
+ xpath_initial = xpath
409
+ xpath = '(' + xpath_initial + f"/following-sibling::{sub_xpath} | " + xpath_initial + f"/preceding-sibling::{sub_xpath})"
410
+
411
+ return xpath
412
+
413
+ except Exception as e:
414
+ print(f"Error occurred during selector conversion: {e}")
415
+ return "//error"
416
+
327
417
  def is_port_in_use(port: int) -> bool:
328
418
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
329
419
  return s.connect_ex(('127.0.0.1', port)) == 0
kea2/utils.py CHANGED
@@ -1,6 +1,9 @@
1
1
  import logging
2
2
  import os
3
3
  from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+ if TYPE_CHECKING:
6
+ from .keaUtils import Options
4
7
 
5
8
 
6
9
  def getLogger(name: str) -> logging.Logger:
@@ -50,4 +53,8 @@ def getProjectRoot():
50
53
  if cur_dir == root:
51
54
  return None
52
55
  cur_dir = cur_dir.parent
53
- return cur_dir
56
+ return cur_dir
57
+
58
+
59
+
60
+