Kea2-python 0.1.1__tar.gz → 0.1.3__tar.gz

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.

Files changed (43) hide show
  1. {kea2_python-0.1.1 → kea2_python-0.1.3}/Kea2_python.egg-info/PKG-INFO +5 -4
  2. {kea2_python-0.1.1 → kea2_python-0.1.3}/Kea2_python.egg-info/SOURCES.txt +3 -1
  3. {kea2_python-0.1.1 → kea2_python-0.1.3}/PKG-INFO +5 -4
  4. {kea2_python-0.1.1 → kea2_python-0.1.3}/README.md +4 -3
  5. kea2_python-0.1.3/kea2/assets/kea2-thirdpart.jar +0 -0
  6. kea2_python-0.1.3/kea2/assets/monkeyq.jar +0 -0
  7. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/quicktest.py +3 -1
  8. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/bug_report_generator.py +3 -1
  9. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/fastbotManager.py +12 -2
  10. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/keaUtils.py +63 -34
  11. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/kea_launcher.py +10 -0
  12. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/u2Driver.py +122 -108
  13. {kea2_python-0.1.1 → kea2_python-0.1.3}/pyproject.toml +1 -1
  14. kea2_python-0.1.3/tests/test_u2Selector.py +200 -0
  15. kea2_python-0.1.1/kea2/assets/monkeyq.jar +0 -0
  16. {kea2_python-0.1.1 → kea2_python-0.1.3}/Kea2_python.egg-info/dependency_links.txt +0 -0
  17. {kea2_python-0.1.1 → kea2_python-0.1.3}/Kea2_python.egg-info/entry_points.txt +0 -0
  18. {kea2_python-0.1.1 → kea2_python-0.1.3}/Kea2_python.egg-info/requires.txt +0 -0
  19. {kea2_python-0.1.1 → kea2_python-0.1.3}/Kea2_python.egg-info/top_level.txt +0 -0
  20. {kea2_python-0.1.1 → kea2_python-0.1.3}/LICENSE +0 -0
  21. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/__init__.py +0 -0
  22. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/absDriver.py +0 -0
  23. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/adbUtils.py +0 -0
  24. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot-thirdpart.jar +0 -0
  25. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/abl.strings +0 -0
  26. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/awl.strings +0 -0
  27. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/max.config +0 -0
  28. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/max.fuzzing.strings +0 -0
  29. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/max.schema.strings +0 -0
  30. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/max.strings +0 -0
  31. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/max.tree.pruning +0 -0
  32. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_configs/widget.block.py +0 -0
  33. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  34. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  35. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  36. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  37. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/assets/framework.jar +0 -0
  38. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/cli.py +0 -0
  39. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/logWatcher.py +0 -0
  40. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/resultSyncer.py +0 -0
  41. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/templates/bug_report_template.html +0 -0
  42. {kea2_python-0.1.1 → kea2_python-0.1.3}/kea2/utils.py +0 -0
  43. {kea2_python-0.1.1 → kea2_python-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A python library for supporting and customizing automated UI testing for mobile apps
5
5
  Author-email: Xixian Liang <xixian@stu.ecnu.edu.cn>
6
6
  Requires-Python: >=3.8
@@ -17,8 +17,9 @@ Dynamic: license-file
17
17
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
18
18
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
19
19
 
20
+
20
21
  <div>
21
- <img src="https://github.com/user-attachments/assets/1a64635b-a8f2-40f1-8f16-55e47b1d74e7" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
+ <img src="https://github.com/user-attachments/assets/aa5839fc-4542-46f6-918b-c9f891356c84" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
23
  </div>
23
24
 
24
25
  ## About
@@ -67,7 +68,7 @@ In the future, Kea2 will be extended to support
67
68
 
68
69
  Running environment:
69
70
  - support Windows, MacOS and Linux
70
- - python 3.8+, Android 4.4+ (Android SDK installed)
71
+ - python 3.8+, Android 5.0+ (Android SDK installed)
71
72
  - **VPN closed** (Features 2 and 3 required)
72
73
 
73
74
  Install Kea2 by `pip`:
@@ -251,4 +252,4 @@ Kea2 has also received many valuable insights, advices, feedbacks and lessons sh
251
252
 
252
253
  [^1]: 不少UI自动化测试工具提供了“自定义事件序列”能力(如[Fastbot](https://github.com/bytedance/Fastbot_Android/blob/main/handbook-cn.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%BA%8B%E4%BB%B6%E5%BA%8F%E5%88%97) 和[AppCrawler](https://github.com/seveniruby/AppCrawler)),但在实际使用中存在不少问题,如自定义能力有限、使用不灵活等。此前不少Fastbot用户抱怨过其“自定义事件序列”在使用中的问题,如[#209](https://github.com/bytedance/Fastbot_Android/issues/209), [#225](https://github.com/bytedance/Fastbot_Android/issues/225), [#286](https://github.com/bytedance/Fastbot_Android/issues/286)等。
253
254
 
254
- [^2]: 在UI自动化测试过程中支持自动断言是一个很重要的能力,但几乎没有测试工具提供这样的能力。我们注意到[AppCrawler](https://ceshiren.com/t/topic/15801/5)的开发者曾经希望提供一种断言机制,得到了用户的热切响应,不少用户从21年就开始催更,但始终未能实现。
255
+ [^2]: 在UI自动化测试过程中支持自动断言是一个很重要的能力,但几乎没有测试工具提供这样的能力。我们注意到[AppCrawler](https://ceshiren.com/t/topic/15801/5)的开发者曾经希望提供一种断言机制,得到了用户的热切响应,不少用户从21年就开始催更,但始终未能实现。
@@ -21,6 +21,7 @@ kea2/u2Driver.py
21
21
  kea2/utils.py
22
22
  kea2/assets/fastbot-thirdpart.jar
23
23
  kea2/assets/framework.jar
24
+ kea2/assets/kea2-thirdpart.jar
24
25
  kea2/assets/monkeyq.jar
25
26
  kea2/assets/quicktest.py
26
27
  kea2/assets/fastbot_configs/abl.strings
@@ -35,4 +36,5 @@ kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so
35
36
  kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so
36
37
  kea2/assets/fastbot_libs/x86/libfastbot_native.so
37
38
  kea2/assets/fastbot_libs/x86_64/libfastbot_native.so
38
- kea2/templates/bug_report_template.html
39
+ kea2/templates/bug_report_template.html
40
+ tests/test_u2Selector.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A python library for supporting and customizing automated UI testing for mobile apps
5
5
  Author-email: Xixian Liang <xixian@stu.ecnu.edu.cn>
6
6
  Requires-Python: >=3.8
@@ -17,8 +17,9 @@ Dynamic: license-file
17
17
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
18
18
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
19
19
 
20
+
20
21
  <div>
21
- <img src="https://github.com/user-attachments/assets/1a64635b-a8f2-40f1-8f16-55e47b1d74e7" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
+ <img src="https://github.com/user-attachments/assets/aa5839fc-4542-46f6-918b-c9f891356c84" style="border-radius: 14px; width: 20%; height: 20%;"/>
22
23
  </div>
23
24
 
24
25
  ## About
@@ -67,7 +68,7 @@ In the future, Kea2 will be extended to support
67
68
 
68
69
  Running environment:
69
70
  - support Windows, MacOS and Linux
70
- - python 3.8+, Android 4.4+ (Android SDK installed)
71
+ - python 3.8+, Android 5.0+ (Android SDK installed)
71
72
  - **VPN closed** (Features 2 and 3 required)
72
73
 
73
74
  Install Kea2 by `pip`:
@@ -251,4 +252,4 @@ Kea2 has also received many valuable insights, advices, feedbacks and lessons sh
251
252
 
252
253
  [^1]: 不少UI自动化测试工具提供了“自定义事件序列”能力(如[Fastbot](https://github.com/bytedance/Fastbot_Android/blob/main/handbook-cn.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%BA%8B%E4%BB%B6%E5%BA%8F%E5%88%97) 和[AppCrawler](https://github.com/seveniruby/AppCrawler)),但在实际使用中存在不少问题,如自定义能力有限、使用不灵活等。此前不少Fastbot用户抱怨过其“自定义事件序列”在使用中的问题,如[#209](https://github.com/bytedance/Fastbot_Android/issues/209), [#225](https://github.com/bytedance/Fastbot_Android/issues/225), [#286](https://github.com/bytedance/Fastbot_Android/issues/286)等。
253
254
 
254
- [^2]: 在UI自动化测试过程中支持自动断言是一个很重要的能力,但几乎没有测试工具提供这样的能力。我们注意到[AppCrawler](https://ceshiren.com/t/topic/15801/5)的开发者曾经希望提供一种断言机制,得到了用户的热切响应,不少用户从21年就开始催更,但始终未能实现。
255
+ [^2]: 在UI自动化测试过程中支持自动断言是一个很重要的能力,但几乎没有测试工具提供这样的能力。我们注意到[AppCrawler](https://ceshiren.com/t/topic/15801/5)的开发者曾经希望提供一种断言机制,得到了用户的热切响应,不少用户从21年就开始催更,但始终未能实现。
@@ -4,8 +4,9 @@
4
4
  [![PyPI Downloads](https://static.pepy.tech/badge/kea2-python)](https://pepy.tech/projects/kea2-python)
5
5
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
6
6
 
7
+
7
8
  <div>
8
- <img src="https://github.com/user-attachments/assets/1a64635b-a8f2-40f1-8f16-55e47b1d74e7" style="border-radius: 14px; width: 20%; height: 20%;"/>
9
+ <img src="https://github.com/user-attachments/assets/aa5839fc-4542-46f6-918b-c9f891356c84" style="border-radius: 14px; width: 20%; height: 20%;"/>
9
10
  </div>
10
11
 
11
12
  ## About
@@ -54,7 +55,7 @@ In the future, Kea2 will be extended to support
54
55
 
55
56
  Running environment:
56
57
  - support Windows, MacOS and Linux
57
- - python 3.8+, Android 4.4+ (Android SDK installed)
58
+ - python 3.8+, Android 5.0+ (Android SDK installed)
58
59
  - **VPN closed** (Features 2 and 3 required)
59
60
 
60
61
  Install Kea2 by `pip`:
@@ -238,4 +239,4 @@ Kea2 has also received many valuable insights, advices, feedbacks and lessons sh
238
239
 
239
240
  [^1]: 不少UI自动化测试工具提供了“自定义事件序列”能力(如[Fastbot](https://github.com/bytedance/Fastbot_Android/blob/main/handbook-cn.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%BA%8B%E4%BB%B6%E5%BA%8F%E5%88%97) 和[AppCrawler](https://github.com/seveniruby/AppCrawler)),但在实际使用中存在不少问题,如自定义能力有限、使用不灵活等。此前不少Fastbot用户抱怨过其“自定义事件序列”在使用中的问题,如[#209](https://github.com/bytedance/Fastbot_Android/issues/209), [#225](https://github.com/bytedance/Fastbot_Android/issues/225), [#286](https://github.com/bytedance/Fastbot_Android/issues/286)等。
240
241
 
241
- [^2]: 在UI自动化测试过程中支持自动断言是一个很重要的能力,但几乎没有测试工具提供这样的能力。我们注意到[AppCrawler](https://ceshiren.com/t/topic/15801/5)的开发者曾经希望提供一种断言机制,得到了用户的热切响应,不少用户从21年就开始催更,但始终未能实现。
242
+ [^2]: 在UI自动化测试过程中支持自动断言是一个很重要的能力,但几乎没有测试工具提供这样的能力。我们注意到[AppCrawler](https://ceshiren.com/t/topic/15801/5)的开发者曾经希望提供一种断言机制,得到了用户的热切响应,不少用户从21年就开始催更,但始终未能实现。
@@ -81,7 +81,9 @@ if __name__ == "__main__":
81
81
  Driver=U2Driver,
82
82
  packageNames=[PACKAGE_NAME],
83
83
  # serial="emulator-5554", # specify the serial
84
- maxStep=5000,
84
+ maxStep=50,
85
+ profile_period=10,
86
+ take_screenshots=True, # whether to take screenshots, default is False
85
87
  # running_mins=10, # specify the maximal running time in minutes, default value is 10m
86
88
  # throttle=200, # specify the throttle in milliseconds, default value is 200ms
87
89
  agent="u2" # 'native' for running the vanilla Fastbot, 'u2' for running Kea2
@@ -68,7 +68,7 @@ class BugReportGenerator:
68
68
  with open(report_path, "w", encoding="utf-8") as f:
69
69
  f.write(html_content)
70
70
 
71
- logger.debug(f"Bug report generated: {report_path}")
71
+ logger.info(f"Bug report saved to: {report_path}")
72
72
 
73
73
  except Exception as e:
74
74
  logger.error(f"Error generating bug report: {e}")
@@ -323,6 +323,8 @@ class BugReportGenerator:
323
323
  if lines:
324
324
  # Collect coverage trend data
325
325
  for line in lines:
326
+ if not line.strip():
327
+ continue
326
328
  try:
327
329
  coverage_data = json.loads(line)
328
330
  data["coverage_trend"].append({
@@ -42,6 +42,11 @@ class FastbotManager:
42
42
  "/sdcard/fastbot-thirdpart.jar",
43
43
  device=options.serial,
44
44
  )
45
+ push_file(
46
+ Path.joinpath(cur_dir, "assets/kea2-thirdpart.jar"),
47
+ "/sdcard/kea2-thirdpart.jar",
48
+ device=options.serial,
49
+ )
45
50
  push_file(
46
51
  Path.joinpath(cur_dir, "assets/framework.jar"),
47
52
  "/sdcard/framework.jar",
@@ -91,7 +96,12 @@ class FastbotManager:
91
96
 
92
97
  def _startFastbotService(self) -> threading.Thread:
93
98
  shell_command = [
94
- "CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar:/sdcard/fastbot-thirdpart.jar",
99
+ "CLASSPATH="
100
+ "/sdcard/monkeyq.jar:"
101
+ "/sdcard/framework.jar:"
102
+ "/sdcard/fastbot-thirdpart.jar:"
103
+ "/sdcard/kea2-thirdpart.jar",
104
+
95
105
  "exec", "app_process",
96
106
  "/system/bin", "com.android.commands.monkey.Monkey",
97
107
  "-p", *self.options.packageNames,
@@ -126,7 +136,7 @@ class FastbotManager:
126
136
  self.return_code = proc.wait()
127
137
  f.close()
128
138
  if self.return_code != 0:
129
- raise RuntimeError(f"Fastbot Error: Terminated with [code {self.return_code}]")
139
+ raise RuntimeError(f"Fastbot Error: Terminated with [code {self.return_code}] See {self.log_file} for details.")
130
140
 
131
141
  def get_return_code(self):
132
142
  if self.thread:
@@ -14,7 +14,7 @@ from .bug_report_generator import BugReportGenerator
14
14
  from .resultSyncer import ResultSyncer
15
15
  from .logWatcher import LogWatcher
16
16
  from .utils import TimeStamp, getProjectRoot, getLogger
17
- from .u2Driver import StaticU2UiObject, selector_to_xpath
17
+ from .u2Driver import StaticU2UiObject
18
18
  from .fastbotManager import FastbotManager
19
19
  import uiautomator2 as u2
20
20
  import types
@@ -126,6 +126,8 @@ class Options:
126
126
  profile_period: int = 25
127
127
  # take screenshots for every step
128
128
  take_screenshots: bool = False
129
+ # The root of output dir on device
130
+ device_output_root: str = "/sdcard"
129
131
  # the debug mode
130
132
  debug: bool = False
131
133
 
@@ -135,10 +137,18 @@ class Options:
135
137
  super().__setattr__(name, value)
136
138
 
137
139
  def __post_init__(self):
140
+ import logging
141
+ logging.basicConfig(level=logging.DEBUG if self.debug else logging.INFO)
138
142
  if self.serial and self.Driver:
139
143
  self.Driver.setDeviceSerial(self.serial)
140
144
  global LOGFILE, RESFILE, STAMP
141
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
+ )
142
152
  STAMP = self.log_stamp
143
153
  self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
144
154
  LOGFILE = f"fastbot_{STAMP}.log"
@@ -324,8 +334,9 @@ class KeaTestRunner(TextTestRunner):
324
334
  xml_raw = self.stepMonkey()
325
335
  propsSatisfiedPrecond = self.getValidProperties(xml_raw, result)
326
336
  except requests.ConnectionError:
337
+ logger.info("Connection refused by remote.")
327
338
  if fb.get_return_code() == 0:
328
- logger.info("[INFO] Exploration times up (--running-minutes).")
339
+ logger.info("Exploration times up (--running-minutes).")
329
340
  end_by_remote = True
330
341
  break
331
342
  raise RuntimeError("Fastbot Aborted.")
@@ -333,8 +344,6 @@ class KeaTestRunner(TextTestRunner):
333
344
  if self.options.profile_period and self.stepsCount % self.options.profile_period == 0:
334
345
  resultSyncer.sync_event.set()
335
346
 
336
- print(f"{len(propsSatisfiedPrecond)} precond satisfied.", flush=True)
337
-
338
347
  # Go to the next round if no precond satisfied
339
348
  if len(propsSatisfiedPrecond) == 0:
340
349
  continue
@@ -467,18 +476,25 @@ class KeaTestRunner(TextTestRunner):
467
476
  if not precond(test):
468
477
  valid = False
469
478
  break
479
+ except u2.UiObjectNotFoundError as e:
480
+ valid = False
481
+ break
470
482
  except Exception as e:
471
- print(f"[ERROR] Error when checking precond: {getFullPropName(test)}", flush=True)
483
+ logger.error(f"Error when checking precond: {getFullPropName(test)}")
472
484
  traceback.print_exc()
473
485
  valid = False
474
486
  break
475
487
  # if all the precond passed. make it the candidate prop.
476
488
  if valid:
477
- logger.debug(f"precond satisfied: {getFullPropName(test)}")
478
489
  if result.getExcuted(test) >= getattr(prop, MAX_TRIES_MARKER, float("inf")):
479
- logger.debug(f"{getFullPropName(test)} has reached its max_tries. Skip.")
490
+ print(f"{getFullPropName(test)} has reached its max_tries. Skip.", flush=True)
480
491
  continue
481
492
  validProps[propName] = test
493
+
494
+ print(f"{len(validProps)} precond satisfied.", flush=True)
495
+ if len(validProps) > 0:
496
+ print("[INFO] Valid properties:",flush=True)
497
+ print("\n".join([f' - {getFullPropName(p)}' for p in validProps.values()]), flush=True)
482
498
  return validProps
483
499
 
484
500
  def _logScript(self, execution_info:Dict):
@@ -495,7 +511,8 @@ class KeaTestRunner(TextTestRunner):
495
511
  URL = f"http://localhost:{self.scriptDriver.lport}/init"
496
512
  data = {
497
513
  "takeScreenshots": self.options.take_screenshots,
498
- "Stamp": STAMP
514
+ "Stamp": STAMP,
515
+ "deviceOutputRoot": self.options.device_output_root,
499
516
  }
500
517
  print(f"[INFO] Init fastbot: {data}", flush=True)
501
518
  r = requests.post(
@@ -505,7 +522,7 @@ class KeaTestRunner(TextTestRunner):
505
522
  res = r.content.decode(encoding="utf-8")
506
523
  import re
507
524
  self.device_output_dir = re.match(r"outputDir:(.+)", res).group(1)
508
- print(f"[INFO] Fastbot initiated. Device outputDir: {res}", flush=True)
525
+ print(f"[INFO] Fastbot initiated. outputDir: {res}", flush=True)
509
526
 
510
527
  def collectAllProperties(self, test: TestSuite):
511
528
  """collect all the properties to prepare for PBT
@@ -607,60 +624,72 @@ class KeaTestRunner(TextTestRunner):
607
624
  """
608
625
  def _get_xpath_widgets(func):
609
626
  blocked_set = set()
610
- try:
611
- script_driver = self.options.Driver.getScriptDriver()
612
- preconds = getattr(func, PRECONDITIONS_MARKER, [])
613
- if all(precond(script_driver) for precond in preconds):
627
+ script_driver = self.options.Driver.getScriptDriver()
628
+ preconds = getattr(func, PRECONDITIONS_MARKER, [])
629
+
630
+ def preconds_pass(preconds):
631
+ try:
632
+ return all(precond(script_driver) for precond in preconds)
633
+ except u2.UiObjectNotFoundError as e:
634
+ return False
635
+ except Exception as e:
636
+ logger.error(f"Error processing precond. Check if precond: {e}")
637
+ traceback.print_exc()
638
+ return False
639
+
640
+ if preconds_pass(preconds):
641
+ try:
614
642
  _widgets = func(self.options.Driver.getStaticChecker())
615
643
  _widgets = _widgets if isinstance(_widgets, list) else [_widgets]
616
644
  for w in _widgets:
617
645
  if isinstance(w, StaticU2UiObject):
618
- xpath = selector_to_xpath(w.selector, True)
619
- blocked_set.add(xpath)
646
+ xpath = w.selector_to_xpath(w.selector)
647
+ if xpath != '//error':
648
+ blocked_set.add(xpath)
620
649
  elif isinstance(w, u2.xpath.XPathSelector):
621
650
  xpath = w._parent.xpath
622
651
  blocked_set.add(xpath)
623
652
  else:
624
- logger.warning(f"{w} Not supported")
625
- except Exception as e:
626
- logger.error(f"Error processing blocked widgets: {e}")
627
- traceback.print_exc()
653
+ logger.error(f"block widget defined in {func.__name__} Not supported.")
654
+ except Exception as e:
655
+ logger.error(f"Error processing blocked widgets in: {func}")
656
+ logger.error(e)
657
+ traceback.print_exc()
628
658
  return blocked_set
629
659
 
630
- res = {
660
+ result = {
631
661
  "widgets": set(),
632
662
  "trees": set()
633
663
  }
634
664
 
635
-
636
665
  for func in self._blockWidgetFuncs["widgets"]:
637
666
  widgets = _get_xpath_widgets(func)
638
- res["widgets"].update(widgets)
639
-
667
+ result["widgets"].update(widgets)
640
668
 
641
669
  for func in self._blockWidgetFuncs["trees"]:
642
670
  trees = _get_xpath_widgets(func)
643
- res["trees"].update(trees)
671
+ result["trees"].update(trees)
644
672
 
673
+ result["widgets"] = list(result["widgets"] - result["trees"])
674
+ result["trees"] = list(result["trees"])
645
675
 
646
- res["widgets"] = list(res["widgets"] - res["trees"])
647
- res["trees"] = list(res["trees"])
648
-
649
- return res
676
+ return result
650
677
 
651
678
 
652
679
  def __del__(self):
653
680
  """tearDown method. Cleanup the env.
654
681
  """
655
- try:
656
- logger.debug("Generating test bug report")
657
- report_generator = BugReportGenerator(self.options.output_dir)
658
- report_generator.generate_report()
659
- except Exception as e:
660
- logger.error(f"Error generating bug report: {e}", flush=True)
661
682
  try:
662
683
  self.stopMonkey()
663
684
  except Exception as e:
664
685
  pass
686
+
665
687
  if self.options.Driver:
666
688
  self.options.Driver.tearDown()
689
+
690
+ try:
691
+ logger.info("Generating bug report")
692
+ report_generator = BugReportGenerator(self.options.output_dir)
693
+ report_generator.generate_report()
694
+ except Exception as e:
695
+ logger.error(f"Error generating bug report: {e}", flush=True)
@@ -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)
@@ -100,39 +100,105 @@ class StaticU2UiObject(u2.UiObject):
100
100
  return filterDict[originKey]
101
101
  return originKey
102
102
 
103
- def _getXPath(self, kwargs: Dict[str, str]):
103
+ def selector_to_xpath(self, selector: u2.Selector, is_initial: bool = True) -> str:
104
+ """
105
+ Convert a u2 Selector into an XPath expression compatible with Java Android UI controls.
104
106
 
105
- def filter_selectors(kwargs: Dict[str, str]):
106
- """
107
- filter the selector
108
- """
109
- new_kwargs = dict()
110
- SPECIAL_KEY = {"mask", "childOrSibling", "childOrSiblingSelector"}
111
- for key, val in kwargs.items():
112
- if key in SPECIAL_KEY:
113
- continue
114
- key = self._transferU2Keys(key)
115
- new_kwargs[key] = val
116
- return new_kwargs
107
+ Args:
108
+ selector (u2.Selector): A u2 Selector object
109
+ is_initial (bool): Whether it is the initial node, defaults to True
117
110
 
118
- kwargs = filter_selectors(kwargs)
111
+ Returns:
112
+ str: The corresponding XPath expression
113
+ """
114
+ try:
119
115
 
120
- attrLocs = [
121
- f"[@{k}='{v}']" for k, v in kwargs.items()
122
- ]
123
- xpath = f".//node{''.join(attrLocs)}"
124
- return xpath
116
+ xpath = ".//node" if is_initial else "node"
117
+
118
+ conditions = []
119
+
120
+ if "className" in selector:
121
+ conditions.insert(0, f"[@class='{selector['className']}']")
122
+
123
+ if "text" in selector:
124
+ conditions.append(f"[@text='{selector['text']}']")
125
+ elif "textContains" in selector:
126
+ conditions.append(f"[contains(@text, '{selector['textContains']}')]")
127
+ elif "textStartsWith" in selector:
128
+ conditions.append(f"[starts-with(@text, '{selector['textStartsWith']}')]")
129
+ elif "textMatches" in selector:
130
+ raise NotImplementedError("'textMatches' syntax is not supported")
131
+
132
+ if "description" in selector:
133
+ conditions.append(f"[@content-desc='{selector['description']}']")
134
+ elif "descriptionContains" in selector:
135
+ conditions.append(f"[contains(@content-desc, '{selector['descriptionContains']}')]")
136
+ elif "descriptionStartsWith" in selector:
137
+ conditions.append(f"[starts-with(@content-desc, '{selector['descriptionStartsWith']}')]")
138
+ elif "descriptionMatches" in selector:
139
+ raise NotImplementedError("'descriptionMatches' syntax is not supported")
140
+
141
+ if "packageName" in selector:
142
+ conditions.append(f"[@package='{selector['packageName']}']")
143
+ elif "packageNameMatches" in selector:
144
+ raise NotImplementedError("'packageNameMatches' syntax is not supported")
145
+
146
+ if "resourceId" in selector:
147
+ conditions.append(f"[@resource-id='{selector['resourceId']}']")
148
+ elif "resourceIdMatches" in selector:
149
+ raise NotImplementedError("'resourceIdMatches' syntax is not supported")
150
+
151
+ bool_props = ["checkable", "checked", "clickable", "longClickable", "scrollable", "enabled", "focusable",
152
+ "focused", "selected", "covered"]
153
+
154
+ def str_to_bool(value):
155
+ """Convert string 'true'/'false' to boolean, or return original value if already boolean"""
156
+ if isinstance(value, str):
157
+ return value.lower() == "true"
158
+ return bool(value)
159
+
160
+ for prop in bool_props:
161
+ if prop in selector:
162
+ bool_value = str_to_bool(selector[prop])
163
+ value = "true" if bool_value else "false"
164
+ conditions.append(f"[@{prop}='{value}']")
165
+
166
+ if "index" in selector:
167
+ conditions.append(f"[@index='{selector['index']}']")
168
+
169
+ xpath += "".join(conditions)
170
+
171
+ if "childOrSibling" in selector and selector["childOrSibling"]:
172
+ for i, relation in enumerate(selector["childOrSibling"]):
173
+ sub_selector = selector["childOrSiblingSelector"][i]
174
+ sub_xpath = self.selector_to_xpath(sub_selector, False)
175
+
176
+ if relation == "child":
177
+ xpath += f"//{sub_xpath}"
178
+ elif relation == "sibling":
179
+ cur_root = xpath
180
+ following_sibling = cur_root + f"/following-sibling::{sub_xpath}"
181
+ preceding_sibling = cur_root + f"/preceding-sibling::{sub_xpath}"
182
+ xpath = f"({following_sibling} | {preceding_sibling})"
183
+ if "instance" in selector:
184
+ xpath = f"({xpath})[{selector['instance'] + 1}]"
185
+
186
+ return xpath
187
+
188
+ except Exception as e:
189
+ print(f"Error occurred during selector conversion: {e}")
190
+ return "//error"
125
191
 
126
192
 
127
193
  @property
128
194
  def exists(self):
129
- dict.update(self.selector, {"covered": "false"})
130
- xpath = self._getXPath(self.selector)
195
+ set_covered_to_deepest_node(self.selector)
196
+ xpath = self.selector_to_xpath(self.selector)
131
197
  matched_widgets = self.session.xml.xpath(xpath)
132
198
  return bool(matched_widgets)
133
199
 
134
200
  def __len__(self):
135
- xpath = self._getXPath(self.selector)
201
+ xpath = self.selector_to_xpath(self.selector)
136
202
  matched_widgets = self.session.xml.xpath(xpath)
137
203
  return len(matched_widgets)
138
204
 
@@ -141,6 +207,9 @@ class StaticU2UiObject(u2.UiObject):
141
207
 
142
208
  def sibling(self, **kwargs):
143
209
  return StaticU2UiObject(self.session, self.selector.clone().sibling(**kwargs))
210
+
211
+ def __getattr__(self, attr):
212
+ return getattr(super(), attr)
144
213
 
145
214
 
146
215
  def _get_bounds(raw_bounds):
@@ -227,7 +296,9 @@ class U2StaticDevice(u2.Device):
227
296
  self._script_driver = script_driver
228
297
 
229
298
  def __call__(self, **kwargs):
230
- return StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
299
+ ui = StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
300
+ ui.jsonrpc = self._script_driver.jsonrpc
301
+ return ui
231
302
 
232
303
  @property
233
304
  def xpath(self) -> u2.xpath.XPathEntry:
@@ -274,7 +345,12 @@ class U2StaticChecker(AbstractStaticChecker):
274
345
  def setHierarchy(self, hierarchy: str):
275
346
  if hierarchy is None:
276
347
  return
277
- self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
348
+ if isinstance(hierarchy, str):
349
+ self.d.xml = etree.fromstring(hierarchy.encode("utf-8"))
350
+ elif isinstance(hierarchy, etree._Element):
351
+ self.d.xml = hierarchy
352
+ elif isinstance(hierarchy, etree._ElementTree):
353
+ self.d.xml = hierarchy.getroot()
278
354
  _HindenWidgetFilter(self.d.xml)
279
355
 
280
356
  def getInstance(self, hierarchy: str=None):
@@ -330,90 +406,6 @@ def forward_port(self, remote: Union[int, str]) -> int:
330
406
  logger.debug(f"forwading port: tcp:{local_port} -> {remote}")
331
407
  return local_port
332
408
 
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
-
417
409
  def is_port_in_use(port: int) -> bool:
418
410
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
419
411
  return s.connect_ex(('127.0.0.1', port)) == 0
@@ -435,3 +427,25 @@ def get_free_port():
435
427
  if not is_port_in_use(port):
436
428
  return port
437
429
  raise RuntimeError("No free port found")
430
+
431
+ def set_covered_to_deepest_node(selector: u2.Selector):
432
+
433
+ def find_deepest_nodes(node):
434
+ deepest_node = None
435
+ is_leaf = True
436
+ if "childOrSibling" in node and node["childOrSibling"]:
437
+ for i, relation in enumerate(node["childOrSibling"]):
438
+ sub_selector = node["childOrSiblingSelector"][i]
439
+ deepest_node = find_deepest_nodes(sub_selector)
440
+ is_leaf = False
441
+
442
+ if is_leaf:
443
+ deepest_node = node
444
+ return deepest_node
445
+
446
+ deepest_node = find_deepest_nodes(selector)
447
+
448
+ if deepest_node is not None:
449
+ dict.update(deepest_node, {"covered": False})
450
+
451
+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Kea2-python"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "A python library for supporting and customizing automated UI testing for mobile apps"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -0,0 +1,200 @@
1
+ import unittest
2
+ from kea2.u2Driver import _HindenWidgetFilter, U2Driver
3
+ from lxml import etree
4
+ from pathlib import Path
5
+
6
+
7
+ XML_PATH = Path(__file__).parent / "hidden_widget_test.xml"
8
+
9
+
10
+ def get_static_checker():
11
+ xml: etree._ElementTree = etree.parse(XML_PATH)
12
+ d = U2Driver.getStaticChecker(xml)
13
+ return d
14
+
15
+
16
+ class TestHiddenWidget(unittest.TestCase):
17
+
18
+ def test_hidden_widget(self):
19
+ d = get_static_checker()
20
+ assert not d(text="微信(690)").exists
21
+
22
+ import unittest
23
+
24
+ class TestSupportedAttributes(unittest.TestCase):
25
+
26
+ def setUp(self):
27
+ self.d = get_static_checker()
28
+
29
+ def test_text(self):
30
+ assert self.d(text="添加朋友").exists
31
+ assert self.d(textContains="朋友").exists
32
+ assert self.d(textStartsWith="添加").exists
33
+
34
+
35
+ def test_class_name(self):
36
+ assert self.d(className="android.widget.Button").exists
37
+
38
+ def test_description(self):
39
+ assert self.d(description="企业微信联系人,,通过手机号搜索企业微信用户").exists
40
+ assert self.d(descriptionContains="微信联系人").exists
41
+ assert self.d(descriptionStartsWith="企业微信").exists
42
+
43
+
44
+ def test_clickable_true(self):
45
+ assert self.d(clickable=True).exists
46
+ assert self.d(clickable=False).exists
47
+ assert self.d(enabled=True).exists
48
+ assert self.d(focusable=True).exists
49
+ assert self.d(focusable=False).exists
50
+ assert self.d(scrollable=False).exists
51
+ assert self.d(checkable=False).exists
52
+ assert self.d(checked=False).exists
53
+ assert self.d(focused=False).exists
54
+ assert self.d(selected=False).exists
55
+ assert self.d(packageName="com.tencent.mm").exists
56
+ assert self.d(resourceId="com.tencent.mm:id/search_ll").exists
57
+
58
+
59
+ def test_combined_text_and_className(self):
60
+ assert self.d(text="添加朋友", className="android.widget.TextView").exists
61
+
62
+ def test_combined_resourceId_and_className(self):
63
+ assert self.d(resourceId="com.tencent.mm:id/search_ll", className="android.widget.LinearLayout").exists
64
+
65
+ def test_combined_attributes_search_button(self):
66
+ assert self.d(text="添加朋友", clickable=False).exists
67
+
68
+ def test_child_element(self):
69
+ assert self.d(resourceId="android:id/list").child(description="手机联系人,,添加通讯录中的朋友").exists
70
+
71
+
72
+ def test_sibling_element(self):
73
+ assert self.d(description="雷达,,添加身边的朋友").sibling(description="手机联系人,,添加通讯录中的朋友").exists
74
+
75
+
76
+
77
+ class TestUnsupportedMethods(unittest.TestCase):
78
+
79
+ def setUp(self):
80
+ self.d = get_static_checker()
81
+
82
+ def test_positional_left_not_supported(self):
83
+ try:
84
+ result = self.d(text="微信(690)").left(text="搜索")
85
+ assert False, "left() method should not be supported"
86
+ except:
87
+ assert True
88
+
89
+ def test_positional_right_not_supported(self):
90
+ try:
91
+ result = self.d(text="微信(690)").right(text="搜索")
92
+ assert False, "right() method should not be supported"
93
+ except:
94
+ assert True
95
+
96
+ def test_positional_up_not_supported(self):
97
+ try:
98
+ result = self.d(text="通讯录").up(text="10:21")
99
+ assert False, "up() method should not be supported"
100
+ except:
101
+ assert True
102
+
103
+ def test_positional_down_not_supported(self):
104
+ try:
105
+ result = self.d(text="10:21").down(text="通讯录")
106
+ assert False, "down() method should not be supported"
107
+ except:
108
+ assert True
109
+
110
+ def test_child_by_text_not_supported(self):
111
+ try:
112
+ result = self.d(resourceId="android:id/list").child_by_text("通讯录")
113
+ assert False, "child_by_text() method should not be supported"
114
+ except:
115
+ assert True
116
+
117
+ def test_child_by_description_not_supported(self):
118
+ try:
119
+ result = self.d(className="android.widget.ListView").child_by_description("扫描")
120
+ assert False, "child_by_description() method should not be supported"
121
+ except:
122
+ assert True
123
+
124
+ def test_child_by_instance_not_supported(self):
125
+ try:
126
+ result = self.d(className="android.widget.LinearLayout").child_by_instance(1)
127
+ assert False, "child_by_instance() method should not be supported"
128
+ except:
129
+ assert True
130
+
131
+ def test_instance_parameter_not_supported(self):
132
+ try:
133
+ result = self.d(className="android.widget.TextView", instance=0)
134
+ assert False, "instance parameter should not be supported"
135
+ except:
136
+ assert True
137
+
138
+ def test_text_matches_not_supported(self):
139
+ try:
140
+ result = self.d(textMatches="微信.*")
141
+ assert False, "textMatches should not be supported"
142
+ except:
143
+ assert True
144
+
145
+ def test_class_name_matches_not_supported(self):
146
+ try:
147
+ result = self.d(classNameMatches=".*TextView")
148
+ assert False, "classNameMatches should not be supported"
149
+ except:
150
+ assert True
151
+
152
+ def test_description_matches_not_supported(self):
153
+ try:
154
+ result = self.d(descriptionMatches=".*搜索.*")
155
+ assert False, "descriptionMatches should not be supported"
156
+ except:
157
+ assert True
158
+
159
+ def test_package_name_matches_not_supported(self):
160
+ try:
161
+ result = self.d(packageNameMatches="com.tencent.*")
162
+ assert False, "packageNameMatches should not be supported"
163
+ except:
164
+ assert True
165
+
166
+ def test_resource_id_matches_not_supported(self):
167
+ try:
168
+ result = self.d(resourceIdMatches=".*:id/.*")
169
+ assert False, "resourceIdMatches should not be supported"
170
+ except:
171
+ assert True
172
+
173
+ class TestEdgeCases(unittest.TestCase):
174
+
175
+ def setUp(self):
176
+ self.d = get_static_checker()
177
+
178
+ def test_empty_text(self):
179
+ assert self.d(text="").exists
180
+
181
+ def test_empty_resource_id(self):
182
+ assert self.d(resourceId="").exists
183
+
184
+ def test_special_characters_in_text(self):
185
+ assert not self.d(text="微信(690)").exists
186
+ assert not self.d(text="[有人@我] 测试: [聊天记录]").exists
187
+
188
+ def test_non_existent_element(self):
189
+ assert not self.d(text="不存在的文本").exists
190
+ assert not self.d(resourceId="com.example.nonexistent").exists
191
+
192
+ class TestWidget(unittest.TestCase):
193
+
194
+ def test_widget(self):
195
+ d = get_static_checker()
196
+
197
+
198
+
199
+ if __name__ == "__main__":
200
+ unittest.main()
Binary file
File without changes
File without changes
File without changes
File without changes