Kea2-python 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kea2/__init__.py CHANGED
@@ -1 +1,3 @@
1
- from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options, interruptable,HybridTestRunner,kea2_breakpoint
1
+ from .keaUtils import KeaTestRunner, precondition, prob, max_tries, Options, interruptable,HybridTestRunner,kea2_breakpoint
2
+ from .kea2_api import Kea2Tester
3
+ from .u2Driver import U2Driver
kea2/assets/monkeyq.jar CHANGED
Binary file
kea2/cli.py CHANGED
@@ -95,13 +95,12 @@ def cmd_merge(args):
95
95
  # Merge test reports
96
96
  merged_report = merger.merge_reports(args.paths, args.output)
97
97
 
98
- # Print results
99
- print(f"✅ Test reports merged successfully!", flush=True)
100
- print(f"📊 Merged report: {merged_report}", flush=True)
101
-
102
- # Get merge summary
103
- merge_summary = merger.get_merge_summary()
104
- print(f"📈 Merged {merge_summary.get('merged_directories', 0)} directories", flush=True)
98
+ if merged_report is not None:
99
+ print(f"✅ Test reports merged successfully!", flush=True)
100
+ print(f"📊 Merged report: {merged_report}", flush=True)
101
+ # Get merge summary
102
+ merge_summary = merger.get_merge_summary()
103
+ print(f"📈 Merged {merge_summary.get('merged_directories', 0)} directories", flush=True)
105
104
 
106
105
  except Exception as e:
107
106
  logger.error(f"Error during merge operation: {e}")
kea2/fastbotManager.py CHANGED
@@ -41,62 +41,12 @@ class FastbotManager:
41
41
  :params: port: the listening port for script driver
42
42
  :return: the fastbot daemon thread
43
43
  """
44
- cur_dir = Path(__file__).parent
45
- self.dev.sync.push(
46
- Path.joinpath(cur_dir, "assets/monkeyq.jar"),
47
- "/sdcard/monkeyq.jar"
48
- )
49
- self.dev.sync.push(
50
- Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
51
- "/sdcard/fastbot-thirdpart.jar",
52
- )
53
- self.dev.sync.push(
54
- Path.joinpath(cur_dir, "assets/kea2-thirdpart.jar"),
55
- "/sdcard/kea2-thirdpart.jar",
56
- )
57
- self.dev.sync.push(
58
- Path.joinpath(cur_dir, "assets/framework.jar"),
59
- "/sdcard/framework.jar",
60
- )
61
- self.dev.sync.push(
62
- Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a/libfastbot_native.so"),
63
- "/data/local/tmp/arm64-v8a/libfastbot_native.so",
64
- )
65
- self.dev.sync.push(
66
- Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a/libfastbot_native.so"),
67
- "/data/local/tmp/armeabi-v7a/libfastbot_native.so",
68
- )
69
- self.dev.sync.push(
70
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86/libfastbot_native.so"),
71
- "/data/local/tmp/x86/libfastbot_native.so",
72
- )
73
- self.dev.sync.push(
74
- Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64/libfastbot_native.so"),
75
- "/data/local/tmp/x86_64/libfastbot_native.so",
76
- )
77
-
78
- cwd = getProjectRoot()
79
- whitelist = self.options.act_whitelist_file
80
- blacklist = self.options.act_blacklist_file
81
- if bool(whitelist) ^ bool(blacklist):
82
- if whitelist:
83
- file_to_push = cwd / 'configs' / 'awl.strings'
84
- remote_path = whitelist
85
- else:
86
- file_to_push = cwd / 'configs' / 'abl.strings'
87
- remote_path = blacklist
88
-
89
- self.dev.sync.push(
90
- file_to_push,
91
- remote_path
92
- )
93
44
 
45
+ self._push_libs()
94
46
  t = self._startFastbotService()
95
47
  logger.info("Running Fastbot...")
96
-
97
48
  return t
98
49
 
99
-
100
50
  def check_alive(self):
101
51
  """
102
52
  check if the script driver and proxy server are alive.
@@ -183,6 +133,58 @@ class FastbotManager:
183
133
  @property
184
134
  def device_output_dir(self):
185
135
  return self._device_output_dir
136
+
137
+ def _push_libs(self):
138
+ logger.info("Pushing Fastbot libraries to device...")
139
+ cur_dir = Path(__file__).parent
140
+ self.dev.sync.push(
141
+ Path.joinpath(cur_dir, "assets/monkeyq.jar"),
142
+ "/sdcard/monkeyq.jar"
143
+ )
144
+ self.dev.sync.push(
145
+ Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
146
+ "/sdcard/fastbot-thirdpart.jar",
147
+ )
148
+ self.dev.sync.push(
149
+ Path.joinpath(cur_dir, "assets/kea2-thirdpart.jar"),
150
+ "/sdcard/kea2-thirdpart.jar",
151
+ )
152
+ self.dev.sync.push(
153
+ Path.joinpath(cur_dir, "assets/framework.jar"),
154
+ "/sdcard/framework.jar",
155
+ )
156
+ self.dev.sync.push(
157
+ Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a/libfastbot_native.so"),
158
+ "/data/local/tmp/arm64-v8a/libfastbot_native.so",
159
+ )
160
+ self.dev.sync.push(
161
+ Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a/libfastbot_native.so"),
162
+ "/data/local/tmp/armeabi-v7a/libfastbot_native.so",
163
+ )
164
+ self.dev.sync.push(
165
+ Path.joinpath(cur_dir, "assets/fastbot_libs/x86/libfastbot_native.so"),
166
+ "/data/local/tmp/x86/libfastbot_native.so",
167
+ )
168
+ self.dev.sync.push(
169
+ Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64/libfastbot_native.so"),
170
+ "/data/local/tmp/x86_64/libfastbot_native.so",
171
+ )
172
+
173
+ cwd = getProjectRoot()
174
+ whitelist = self.options.act_whitelist_file
175
+ blacklist = self.options.act_blacklist_file
176
+ if bool(whitelist) ^ bool(blacklist):
177
+ if whitelist:
178
+ file_to_push = cwd / 'configs' / 'awl.strings'
179
+ remote_path = whitelist
180
+ else:
181
+ file_to_push = cwd / 'configs' / 'abl.strings'
182
+ remote_path = blacklist
183
+
184
+ self.dev.sync.push(
185
+ file_to_push,
186
+ remote_path
187
+ )
186
188
 
187
189
  def _startFastbotService(self) -> ADBStreamShell_V2:
188
190
  shell_command = [
kea2/kea2_api.py ADDED
@@ -0,0 +1,166 @@
1
+ from typing import Optional, List, Callable, Any, Union, Dict
2
+ from pathlib import Path
3
+ import unittest
4
+ import os
5
+ import inspect
6
+ from .keaUtils import KeaTestRunner, Options
7
+ from .u2Driver import U2Driver
8
+ from .utils import getLogger, TimeStamp, setCustomProjectRoot
9
+ from .adbUtils import ADBDevice
10
+
11
+ logger = getLogger(__name__)
12
+
13
+
14
+ class Kea2Tester:
15
+ """
16
+ Kea2 property tester
17
+
18
+ This class allows users to directly launch Kea2 property tests in existing test scripts.
19
+
20
+ Environment Variables:
21
+ KEA2_HYBRID_MODE: Controls whether to enable Kea2 testing
22
+ - "kea2": Enable Kea2 testing, trigger a breakpoint after testing is completed
23
+ - Other values or not set: Skip Kea2 testing, continue executing the original script
24
+
25
+ """
26
+
27
+ def __init__(self):
28
+ self.options: Optional[Options] = None
29
+ self.properties: List[unittest.TestCase] = []
30
+ self._caller_info: Optional[Dict[str, str]] = None
31
+
32
+ def run_kea2_testing(self, option: Options, configs_path: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
33
+ """
34
+ Launch kea2 property test
35
+
36
+ Args:
37
+ option: Kea2 and Fastbot configuration options
38
+ configs_path: Your configs directory (absolute or relative path)
39
+
40
+ Returns:
41
+ dict: Test result dictionary containing the following keys:
42
+ - executed (bool): Whether Kea2 testing was executed
43
+ - skipped (bool): Whether Kea2 testing was skipped (KEA2_HYBRID_MODE != kea2)
44
+ - caller_info (Dict|None): Caller information (file, class, method name)
45
+ - output_dir (Path|None): Test output directory
46
+ - bug_report (Path|None): Bug report HTML file path
47
+ - result_json (Path|None): Test result JSON file path
48
+ - log_file (Path|None): Fastbot log file path
49
+
50
+ """
51
+
52
+ self._caller_info = self._get_caller_info()
53
+
54
+ logger.info("Starting Kea2 property testing...")
55
+ logger.info(f"Kea2 test launch location:")
56
+ if self._caller_info:
57
+ logger.info(f" File: {self._caller_info['file']}")
58
+ logger.info(f" Class: {self._caller_info['class']}")
59
+ logger.info(f" Method: {self._caller_info['method']}")
60
+
61
+ self.options = option
62
+ if self.options is None:
63
+ raise ValueError("Please set up the option config first.")
64
+
65
+ from kea2.utils import getProjectRoot
66
+ previous_root = getProjectRoot()
67
+ if configs_path is not None:
68
+ configs_dir = Path(configs_path).expanduser()
69
+ if not configs_dir.exists() or not configs_dir.is_dir():
70
+ raise FileNotFoundError(f"Configs directory not found in the specified path: {configs_dir}")
71
+ else:
72
+ setCustomProjectRoot(configs_path)
73
+
74
+ KeaTestRunner.setOptions(self.options)
75
+ argv = ["python3 -m unittest"] + self.options.propertytest_args
76
+
77
+ logger.info("Starting Kea2 property test...")
78
+ runner = KeaTestRunner()
79
+ unittest.main(module=None, argv=argv, testRunner=runner, exit=False)
80
+ logger.info("Kea2 property test completed.")
81
+
82
+ if configs_path is not None:
83
+ setCustomProjectRoot(previous_root)
84
+
85
+ result = self._build_test_result()
86
+
87
+ return result
88
+
89
+ def _build_test_result(self) -> Dict[str, Any]:
90
+ """
91
+ build test result dict
92
+
93
+ Returns:
94
+ dict: Dictionary containing output directory and paths to various report files
95
+ """
96
+ if self.options is None:
97
+ return {
98
+ 'executed': False,
99
+ 'skipped': False,
100
+ 'caller_info': self._caller_info,
101
+ 'output_dir': None,
102
+ 'bug_report': None,
103
+ 'result_json': None,
104
+ 'log_file': None
105
+ }
106
+
107
+ output_dir = self.options.output_dir
108
+
109
+ from .keaUtils import STAMP, LOGFILE, RESFILE
110
+
111
+ bug_report_path = output_dir / "bug_report.html"
112
+ result_json_path = output_dir / RESFILE.name if hasattr(RESFILE, 'name') else output_dir / f"result_{STAMP}.json"
113
+ log_file_path = output_dir / LOGFILE.name if hasattr(LOGFILE, 'name') else output_dir / f"fastbot_{STAMP}.log"
114
+
115
+ return {
116
+ 'executed': True,
117
+ 'skipped': False,
118
+ 'caller_info': self._caller_info,
119
+ 'output_dir': output_dir,
120
+ 'bug_report': bug_report_path if bug_report_path.exists() else None,
121
+ 'result_json': result_json_path if result_json_path.exists() else None,
122
+ 'log_file': log_file_path if log_file_path.exists() else None
123
+ }
124
+
125
+ def _get_caller_info(self) -> Dict[str, str]:
126
+ """
127
+ Get caller information (file, class, method name)
128
+
129
+ Returns:
130
+ dict: Dictionary containing file, class, method
131
+ """
132
+ try:
133
+ frame = inspect.currentframe()
134
+ caller_frame = frame.f_back.f_back
135
+
136
+ while caller_frame:
137
+ frame_info = inspect.getframeinfo(caller_frame)
138
+ if 'kea2_api.py' not in frame_info.filename:
139
+ # find caller
140
+ file_path = frame_info.filename
141
+ method_name = frame_info.function
142
+
143
+ # get class name
144
+ class_name = None
145
+ if 'self' in caller_frame.f_locals:
146
+ class_name = caller_frame.f_locals['self'].__class__.__name__
147
+
148
+ return {
149
+ 'file': file_path,
150
+ 'class': class_name or 'N/A',
151
+ 'method': method_name
152
+ }
153
+ caller_frame = caller_frame.f_back
154
+
155
+ return {
156
+ 'file': 'Unknown',
157
+ 'class': 'N/A',
158
+ 'method': 'Unknown'
159
+ }
160
+ except Exception as e:
161
+ logger.warning(f"Failed to get caller info: {e}")
162
+ return {
163
+ 'file': 'Unknown',
164
+ 'class': 'N/A',
165
+ 'method': 'Unknown'
166
+ }
kea2/keaUtils.py CHANGED
@@ -23,7 +23,7 @@ from .report.bug_report_generator import BugReportGenerator
23
23
  from .resultSyncer import ResultSyncer
24
24
  from .logWatcher import LogWatcher
25
25
  from .utils import TimeStamp, catchException, getProjectRoot, getLogger, loadFuncsFromFile, timer
26
- from .u2Driver import StaticU2UiObject, StaticXpathUiObject, U2Driver
26
+ from .u2Driver import StaticU2UiObject, StaticXpathObject, U2Driver
27
27
  from .fastbotManager import FastbotManager
28
28
  from .adbUtils import ADBDevice
29
29
  from .mixin import BetterConsoleLogExtensionMixin
@@ -45,7 +45,7 @@ PropName = NewType("PropName", str)
45
45
  PropertyStore = NewType("PropertyStore", Dict[PropName, TestCase])
46
46
 
47
47
 
48
- STAMP = TimeStamp().getTimeStamp()
48
+ STAMP: str
49
49
  LOGFILE: str
50
50
  RESFILE: str
51
51
  PROP_EXEC_RESFILE: str
@@ -118,7 +118,7 @@ class Options:
118
118
  # the driver_name in script (if self.d, then d.)
119
119
  driverName: str = None
120
120
  # the driver (only U2Driver available now)
121
- Driver: AbstractDriver = None
121
+ Driver: AbstractDriver = U2Driver
122
122
  # list of package names. Specify the apps under test
123
123
  packageNames: List[str] = None
124
124
  # target device
@@ -154,7 +154,7 @@ class Options:
154
154
  # Activity BlackList File
155
155
  act_blacklist_file: str = None
156
156
  # propertytest sub-commands args (eg. discover -s xxx -p xxx)
157
- propertytest_args: str = None
157
+ propertytest_args: List[str] = None
158
158
  # unittest sub-commands args (Feat 4)
159
159
  unittest_args: List[str] = None
160
160
  # Extra args (directly passed to fastbot)
@@ -172,10 +172,11 @@ class Options:
172
172
  if self.Driver:
173
173
  self._set_driver()
174
174
 
175
- if self.log_stamp:
176
- self._sanitize_custom_stamp()
177
-
178
175
  global STAMP
176
+ STAMP = self.log_stamp if self.log_stamp else TimeStamp().getTimeStamp()
177
+
178
+ self._sanitize_stamp()
179
+
179
180
  self.output_dir = Path(self.output_dir).absolute() / f"res_{STAMP}"
180
181
  self.set_stamp()
181
182
 
@@ -193,15 +194,14 @@ class Options:
193
194
  RESFILE = f"result_{STAMP}.json"
194
195
  PROP_EXEC_RESFILE = f"property_exec_info_{STAMP}.json"
195
196
 
196
- def _sanitize_custom_stamp(self):
197
+ def _sanitize_stamp(self):
197
198
  global STAMP
198
199
  illegal_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\n', '\r', '\t', '\0']
199
200
  for char in illegal_chars:
200
- if char in self.log_stamp:
201
+ if char in STAMP:
201
202
  raise ValueError(
202
- f"char: `{char}` is illegal in --log-stamp. current stamp: {self.log_stamp}"
203
+ f"char: `{char}` is illegal in --log-stamp. current stamp: {STAMP}"
203
204
  )
204
- STAMP = self.log_stamp
205
205
 
206
206
  def _sanitize_args(self):
207
207
  if not self.take_screenshots and self.pre_failure_screenshots > 0:
@@ -255,7 +255,7 @@ def _check_package_installation(packageNames):
255
255
  for package in packageNames:
256
256
  if package not in installed_packages:
257
257
  logger.error(f"package {package} not installed. Abort.")
258
- raise ValueError("package not installed")
258
+ raise ValueError(f"{package} not installed")
259
259
 
260
260
 
261
261
  def _save_bug_report_configs(options: Options):
@@ -542,6 +542,11 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
542
542
  log_watcher.close()
543
543
 
544
544
  result.logSummary()
545
+
546
+ if self.options.agent == "u2":
547
+ self._generate_bug_report()
548
+
549
+ self.tearDown()
545
550
  return result
546
551
 
547
552
  def shouldStop(self, start_time):
@@ -554,7 +559,7 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
554
559
  r = self._get_block_widgets()
555
560
  r["steps_count"] = self.stepsCount
556
561
  return r
557
-
562
+
558
563
  def _get_block_widgets(self):
559
564
  block_dict = self._getBlockedWidgets()
560
565
  block_widgets: List[str] = block_dict['widgets']
@@ -600,6 +605,8 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
600
605
  continue
601
606
  validProps[propName] = test
602
607
 
608
+ staticCheckerDriver.clear_cache()
609
+
603
610
  print(f"{len(validProps)} precond satisfied.", flush=True)
604
611
  if len(validProps) > 0:
605
612
  print("[INFO] Valid properties:",flush=True)
@@ -731,7 +738,7 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
731
738
  _widgets = func(U2Driver.getStaticChecker())
732
739
  _widgets = _widgets if isinstance(_widgets, list) else [_widgets]
733
740
  for w in _widgets:
734
- if isinstance(w, (StaticU2UiObject, StaticXpathUiObject)):
741
+ if isinstance(w, (StaticU2UiObject, StaticXpathObject)):
735
742
  xpath = w.selector_to_xpath(w.selector)
736
743
  if xpath != '//error':
737
744
  blocked_set.add(xpath)
@@ -767,14 +774,20 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter):
767
774
  logger.info("Generating bug report")
768
775
  BugReportGenerator(self.options.output_dir).generate_report()
769
776
 
770
- def __del__(self):
777
+ def tearDown(self):
771
778
  """tearDown method. Cleanup the env.
772
779
  """
773
780
  if self.options.Driver:
774
781
  self.options.Driver.tearDown()
775
-
776
- if self.options.agent == "u2":
777
- self._generate_bug_report()
782
+
783
+ def __del__(self):
784
+ """tearDown method. Cleanup the env.
785
+ """
786
+ try:
787
+ self.tearDown()
788
+ except Exception:
789
+ # Ignore exceptions in __del__ to avoid "Exception ignored" warnings
790
+ pass
778
791
 
779
792
 
780
793
  class KeaTextTestResult(BetterConsoleLogExtensionMixin, TextTestResult):
@@ -948,8 +961,12 @@ class HybridTestRunner(TextTestRunner, KeaOptionSetter):
948
961
  def __del__(self):
949
962
  """tearDown method. Cleanup the env.
950
963
  """
951
- if self.options.Driver:
952
- self.options.Driver.tearDown()
964
+ try:
965
+ if hasattr(self, 'options') and self.options and self.options.Driver:
966
+ self.options.Driver.tearDown()
967
+ except Exception:
968
+ # Ignore exceptions in __del__ to avoid "Exception ignored" warnings
969
+ pass
953
970
 
954
971
 
955
972
  def kea2_breakpoint():
kea2/logWatcher.py CHANGED
@@ -46,7 +46,7 @@ class LogWatcher:
46
46
  raise RuntimeError(
47
47
  "[Error] Fatal Execption while running fastbot:\n" +
48
48
  exception_body +
49
- "\nSee fastbot.log for details."
49
+ f"\nSee {self.log_file} for details."
50
50
  )
51
51
 
52
52
  statistic_match = PATTERN_STATISTIC.search(content)
@@ -280,7 +280,7 @@ class BugReportGenerator(CrashAnrMixin, PathParserMixin, ScreenshotsMixin):
280
280
  info = step_data.get("Info", {})
281
281
 
282
282
  # Count Monkey events separately
283
- if step_type == "Monkey":
283
+ if step_type == "Monkey" or step_type == "Fuzz":
284
284
  monkey_events_count += 1
285
285
 
286
286
  # If screenshots are enabled, mark the screenshot
@@ -20,9 +20,10 @@ class TestReportMerger:
20
20
  def __init__(self):
21
21
  self.merged_data = {}
22
22
  self.result_dirs = []
23
+ self._package_name: Optional[str] = None
23
24
 
24
25
  @catchException("Error merging reports")
25
- def merge_reports(self, result_paths: List[Union[str, Path]], output_dir: Optional[Union[str, Path]] = None) -> Path:
26
+ def merge_reports(self, result_paths: List[Union[str, Path]], output_dir: Optional[Union[str, Path]] = None) -> Optional[Path]:
26
27
  """
27
28
  Merge multiple test result directories
28
29
 
@@ -31,10 +32,17 @@ class TestReportMerger:
31
32
  output_dir: Output directory for merged data (optional)
32
33
 
33
34
  Returns:
34
- Path to the merged data directory
35
+ Path to the merged data directory, or None if validation fails
35
36
  """
36
37
  # Convert paths and validate
37
38
  self.result_dirs = [Path(p).resolve() for p in result_paths]
39
+ self._package_name = None
40
+
41
+ package_name, fatal_error = self._determine_package_name()
42
+ if fatal_error:
43
+ logger.error("Aborting merge because package validation failed.")
44
+ return None
45
+ self._package_name = package_name
38
46
 
39
47
  # Setup output directory
40
48
  timestamp = datetime.now().strftime("%Y%m%d%H_%M%S")
@@ -59,7 +67,8 @@ class TestReportMerger:
59
67
  final_data['merge_info'] = {
60
68
  'merge_timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
61
69
  'source_count': len(self.result_dirs),
62
- 'source_directories': [str(Path(d).name) for d in self.result_dirs]
70
+ 'source_directories': [str(Path(d).name) for d in self.result_dirs],
71
+ 'package_name': self._package_name or ""
63
72
  }
64
73
 
65
74
  # Generate HTML report (now includes merge info)
@@ -67,6 +76,79 @@ class TestReportMerger:
67
76
 
68
77
  logger.debug(f"Reports generated successfully in: {output_dir}")
69
78
  return report_file
79
+
80
+ def _determine_package_name(self) -> Tuple[Optional[str], bool]:
81
+ """
82
+ Ensure all reports belong to the same application and return the shared package name.
83
+
84
+ Returns:
85
+ tuple: (package_name, fatal_error)
86
+ package_name: shared package name if determined, otherwise None
87
+ fatal_error: True if validation should stop the merge
88
+ """
89
+ if not self.result_dirs:
90
+ logger.error("No result directories provided for merge.")
91
+ return None, True
92
+
93
+ known_package: Optional[str] = None
94
+
95
+ for result_dir in self.result_dirs:
96
+ package_name, fatal_error = self._extract_package_name(result_dir)
97
+ if fatal_error:
98
+ return None, True
99
+ if package_name is None:
100
+ continue
101
+
102
+ if known_package is None:
103
+ known_package = package_name
104
+ elif package_name != known_package:
105
+ logger.error(
106
+ f"Cannot merge reports generated for different applications: "
107
+ f"{result_dir.name} uses package '{package_name}' while others use '{known_package}'."
108
+ )
109
+ return None, True
110
+
111
+ if known_package:
112
+ logger.debug(f"Validated application package for merge: {known_package}")
113
+ else:
114
+ logger.warning("No package information found in provided report directories. Proceeding without package validation.")
115
+ return known_package, False
116
+
117
+ def _extract_package_name(self, result_dir: Path) -> Tuple[Optional[str], bool]:
118
+ """
119
+ Extract the application package name from a report directory.
120
+ """
121
+ config_path = result_dir / "bug_report_config.json"
122
+ if not config_path.exists():
123
+ logger.warning(f"Skipping package validation for {result_dir}: bug_report_config.json not found.")
124
+ return None, False
125
+
126
+ try:
127
+ with open(config_path, "r", encoding="utf-8") as config_file:
128
+ config_data = json.load(config_file)
129
+ except Exception as exc:
130
+ logger.error(f"Failed to load bug_report_config.json from {result_dir}: {exc}")
131
+ return None, True
132
+ package_names = config_data.get("packageNames")
133
+ if isinstance(package_names, str):
134
+ package_name = package_names.strip()
135
+ if not package_name:
136
+ logger.error(f"Package name is empty in bug_report_config.json for {result_dir}")
137
+ return None, True
138
+ return package_name, False
139
+
140
+ if isinstance(package_names, list):
141
+ valid_names = [pkg.strip() for pkg in package_names if pkg and pkg.strip()]
142
+ if not valid_names:
143
+ logger.error(f"No valid packageNames found in bug_report_config.json for {result_dir}")
144
+ return None, True
145
+ if len(valid_names) > 1:
146
+ logger.error(f"Multiple packageNames found in {config_path}, only single package is supported.")
147
+ return None, True
148
+ return valid_names[0], False
149
+
150
+ logger.error(f"packageNames format is invalid in {config_path}")
151
+ return None, True
70
152
 
71
153
  def _merge_property_results(self, output_dir: Path = None) -> Tuple[Dict[str, Dict], Dict[str, List[Dict]]]:
72
154
  """
@@ -663,14 +745,17 @@ class TestReportMerger:
663
745
  if not self.result_dirs:
664
746
  return {}
665
747
 
666
- return {
748
+ summary = {
667
749
  "merged_directories": len(self.result_dirs),
668
750
  "source_paths": [str(p) for p in self.result_dirs],
669
751
  "merge_timestamp": datetime.now().isoformat()
670
752
  }
753
+ if self._package_name:
754
+ summary["package_name"] = self._package_name
755
+ return summary
671
756
 
672
757
  @catchException("Error generating HTML report")
673
- def _generate_html_report(self, data: Dict, output_dir: Path) -> str:
758
+ def _generate_html_report(self, data: Dict, output_dir: Path) -> Path:
674
759
  """
675
760
  Generate HTML report using the merged template
676
761
 
@@ -709,4 +794,4 @@ class TestReportMerger:
709
794
  f.write(html_content)
710
795
 
711
796
  logger.debug(f"HTML report generated: {report_file}")
712
- return str(report_file)
797
+ return report_file
kea2/u2Driver.py CHANGED
@@ -1,19 +1,20 @@
1
1
  import functools
2
- import random
3
- import socket
4
2
  from time import sleep
3
+ from importlib.metadata import version
4
+
5
5
  import uiautomator2 as u2
6
6
  import adbutils
7
7
  import types
8
8
  import rtree
9
9
  import re
10
+
10
11
  from typing import List, Literal, Union, Optional
11
12
  from lxml import etree
13
+ from packaging.version import Version
12
14
  from .absDriver import AbstractScriptDriver, AbstractStaticChecker, AbstractDriver
13
- from .adbUtils import list_forwards, remove_forward, create_forward
14
- from .utils import TimeStamp, getLogger
15
+ from .adbUtils import list_forwards, remove_forward
16
+ from .utils import getLogger
15
17
 
16
- TIME_STAMP = TimeStamp().getTimeStamp()
17
18
 
18
19
  import logging
19
20
  logging.getLogger("urllib3").setLevel(logging.INFO)
@@ -55,8 +56,7 @@ class U2ScriptDriver(AbstractScriptDriver):
55
56
  print("[INFO] Connecting to uiautomator2. Please wait ...")
56
57
  self.d = u2.connect(adb)
57
58
  sleep(5)
58
- self.d._device_server_port = 8090
59
-
59
+ self.d._device_server_port = 8090
60
60
  return self.d
61
61
 
62
62
  def _remove_remote_port(self, port:int):
@@ -69,11 +69,16 @@ class U2ScriptDriver(AbstractScriptDriver):
69
69
  remove_forward(local_spec=forward_local, device=self.deviceSerial)
70
70
 
71
71
  def tearDown(self):
72
- # logger.debug("U2Driver tearDown: stop_uiautomator")
73
- # self.d.stop_uiautomator()
74
- # logger.debug("U2Driver tearDown: remove forward")
75
- # self._remove_remote_port(8090)
76
- pass
72
+ logger.debug("U2Driver tearDown: stop_uiautomator")
73
+ if self.d is None:
74
+ return
75
+ try:
76
+ self.d._device_server_port = 9008
77
+ self.d.stop_uiautomator()
78
+ except (OSError, AttributeError, RuntimeError) as e:
79
+ logger.debug(f"Error during uiautomator teardown (may be already closed): {e}")
80
+ except Exception as e:
81
+ logger.warning(f"Unexpected error during uiautomator teardown: {e}")
77
82
 
78
83
  """
79
84
  The definition of U2StaticChecker
@@ -205,10 +210,14 @@ class StaticU2UiObject(u2.UiObject):
205
210
  def __getattr__(self, attr):
206
211
  return getattr(super(), attr)
207
212
 
213
+
214
+ class StaticXpathObject(u2.xpath.XPathSelector):
215
+ pass
216
+
208
217
  """
209
218
  The definition of XpathStaticChecker
210
219
  """
211
- class StaticXpathUiObject(u2.xpath.XPathSelector):
220
+ class StaticXpathObjectV1(StaticXpathObject):
212
221
  def __init__(self, session, selector):
213
222
  self.session: U2StaticDevice = session
214
223
  self.selector = selector
@@ -218,7 +227,7 @@ class StaticXpathUiObject(u2.xpath.XPathSelector):
218
227
  source = self.session.get_page_source()
219
228
  return len(self.selector.all(source)) > 0
220
229
 
221
- def __and__(self, value) -> 'StaticXpathUiObject':
230
+ def __and__(self, value) -> 'StaticXpathObject':
222
231
  s = u2.xpath.XPathSelector(self.selector)
223
232
  s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
224
233
  s._operator = u2.xpath.Operator.AND
@@ -226,7 +235,7 @@ class StaticXpathUiObject(u2.xpath.XPathSelector):
226
235
  self.selector = s
227
236
  return self
228
237
 
229
- def __or__(self, value) -> 'StaticXpathUiObject':
238
+ def __or__(self, value) -> 'StaticXpathObject':
230
239
  s = u2.xpath.XPathSelector(self.selector)
231
240
  s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
232
241
  s._operator = u2.xpath.Operator.OR
@@ -262,7 +271,7 @@ class StaticXpathUiObject(u2.xpath.XPathSelector):
262
271
  print("Unsupported operator: {}".format(selector._operator))
263
272
  return "//error"
264
273
 
265
- def xpath(self, _xpath: Union[list, tuple, str]) -> 'StaticXpathUiObject':
274
+ def xpath(self, _xpath: Union[list, tuple, str]) -> 'StaticXpathObject':
266
275
  """
267
276
  add xpath to condition list
268
277
  the element should match all conditions
@@ -275,7 +284,7 @@ class StaticXpathUiObject(u2.xpath.XPathSelector):
275
284
  self.selector = self.selector & _xpath
276
285
  return self
277
286
 
278
- def child(self, _xpath: str) -> "StaticXpathUiObject":
287
+ def child(self, _xpath: str) -> "StaticXpathObject":
279
288
  """
280
289
  add child xpath
281
290
  """
@@ -322,6 +331,45 @@ class StaticXpathUiObject(u2.xpath.XPathSelector):
322
331
  raise AttributeError("Invalid attr", key)
323
332
  return getattr(super(), key)
324
333
 
334
+
335
+ class StaticXpathObjectV2(StaticXpathObjectV1):
336
+ def __and__(self, value) -> 'StaticXpathObject':
337
+ s = u2.xpath.XPathSelector(self.selector)
338
+ s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
339
+ s._operator = u2.xpath.Operator.AND
340
+ self.selector = s
341
+ return self
342
+
343
+ def __or__(self, value) -> 'StaticXpathObject':
344
+ s = u2.xpath.XPathSelector(self.selector)
345
+ s._next_xpath = u2.xpath.XPathSelector.create(value.selector)
346
+ s._operator = u2.xpath.Operator.OR
347
+ self.selector = s
348
+ return self
349
+
350
+ def get_last_match(self) -> "u2.xpath.XMLElement":
351
+ source = self.session.get_page_source()
352
+ return self.selector.all(source)[0]
353
+
354
+
355
+ class StaticXpathUiObjectFactory:
356
+
357
+ _u2_version = None
358
+
359
+ @classmethod
360
+ def get_u2_version(cls):
361
+ if cls._u2_version is None:
362
+ cls._u2_version = Version(version("uiautomator2"))
363
+ return cls._u2_version
364
+
365
+ @classmethod
366
+ def create(cls, session, xpath, source) -> StaticXpathObject:
367
+ if cls.get_u2_version() <= Version("3.4.0"):
368
+ return StaticXpathObjectV1(session, selector=u2.xpath.XPathSelector(xpath, source=source))
369
+ elif cls.get_u2_version() >= Version("3.4.1"):
370
+ return StaticXpathObjectV2(session, selector=u2.xpath.XPathSelector(xpath))
371
+
372
+
325
373
  def _get_bounds(raw_bounds):
326
374
  pattern = re.compile(r"\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]")
327
375
  m = re.match(pattern, raw_bounds)
@@ -411,9 +459,11 @@ class _HindenWidgetFilter:
411
459
 
412
460
 
413
461
  class U2StaticDevice(u2.Device):
462
+
414
463
  def __init__(self, script_driver=None):
415
464
  self.xml: etree._Element = None
416
- self._script_driver = script_driver
465
+ self._script_driver:u2.Device = script_driver
466
+ self._app_current = None
417
467
 
418
468
  def __call__(self, **kwargs):
419
469
  ui = StaticU2UiObject(session=self, selector=u2.Selector(**kwargs))
@@ -421,6 +471,14 @@ class U2StaticDevice(u2.Device):
421
471
  ui.jsonrpc = self._script_driver.jsonrpc
422
472
  return ui
423
473
 
474
+ def clear_cache(self):
475
+ self._app_current = None
476
+
477
+ def app_current(self):
478
+ if not self._app_current:
479
+ self._app_current = self._script_driver.app_current()
480
+ return self._app_current
481
+
424
482
  @property
425
483
  def xpath(self) -> u2.xpath.XPathEntry:
426
484
  def get_page_source(self):
@@ -432,32 +490,33 @@ class U2StaticDevice(u2.Device):
432
490
  get_page_source, xpathEntry
433
491
  )
434
492
  return xpathEntry
435
-
493
+
436
494
  def __getattr__(self, attr):
437
495
  """Proxy other methods to script_driver"""
438
496
  logger.debug(f"{attr} not exists in static checker, proxy to script_driver.")
439
497
  return getattr(self._script_driver, attr)
440
498
 
499
+
441
500
  class _XPathEntry(u2.xpath.XPathEntry):
442
501
  def __init__(self, d):
443
502
  self.xpath = None
444
503
  super().__init__(d)
445
-
504
+
446
505
  # def __call__(self, xpath, source = None):
447
506
  # TODO fully support xpath in widget.block.py
448
507
  # self.xpath = xpath
449
508
  # return super().__call__(xpath, source)
509
+
450
510
  def __call__(self, xpath, source=None):
451
- ui = StaticXpathUiObject(session=self, selector=u2.xpath.XPathSelector(xpath, source=source))
511
+ ui = StaticXpathUiObjectFactory.create(session=self, xpath=xpath, source=source)
452
512
  return ui
453
513
 
454
514
 
455
-
456
515
  class U2StaticChecker(AbstractStaticChecker):
457
516
  """
458
517
  This is the StaticChecker used to check the precondition.
459
518
  We use the static checker due to the performing issues when runing multi-properties.
460
-
519
+
461
520
  *e.g. the following self.d use U2StaticChecker*
462
521
  ```
463
522
  @precondition(lambda self: self.d("battery").exists)
@@ -521,51 +580,15 @@ class U2Driver(AbstractDriver):
521
580
  @classmethod
522
581
  def tearDown(self):
523
582
  if self.scriptDriver:
524
- self.scriptDriver.tearDown()
583
+ try:
584
+ self.scriptDriver.tearDown()
585
+ except Exception as e:
586
+ logger.debug(f"Error during U2Driver teardown: {e}")
525
587
 
526
588
 
527
589
  """
528
590
  Other Utils
529
591
  """
530
- def forward_port(self, remote: Union[int, str]) -> int:
531
- """forward remote port to local random port"""
532
- remote = 8090
533
- if isinstance(remote, int):
534
- remote = "tcp:" + str(remote)
535
- for f in self.forward_list():
536
- if (
537
- f.serial == self._serial
538
- and f.remote == remote
539
- and f.local.startswith("tcp:")
540
- ): # yapf: disable
541
- return int(f.local[len("tcp:"):])
542
- local_port = get_free_port()
543
- self.forward("tcp:" + str(local_port), remote)
544
- logger.debug(f"forwading port: tcp:{local_port} -> {remote}")
545
- return local_port
546
-
547
- def is_port_in_use(port: int) -> bool:
548
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
549
- return s.connect_ex(('127.0.0.1', port)) == 0
550
-
551
-
552
- def get_free_port():
553
- try:
554
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
555
- s.bind(('127.0.0.1', 0))
556
- try:
557
- return s.getsockname()[1]
558
- finally:
559
- s.close()
560
- except OSError:
561
- # bind 0 will fail on Manjaro, fallback to random port
562
- # https://github.com/openatx/adbutils/issues/85
563
- for _ in range(20):
564
- port = random.randint(10000, 20000)
565
- if not is_port_in_use(port):
566
- return port
567
- raise RuntimeError("No free port found")
568
-
569
592
  def set_covered_to_deepest_node(selector: u2.Selector):
570
593
 
571
594
  def find_deepest_nodes(node):
@@ -585,5 +608,3 @@ def set_covered_to_deepest_node(selector: u2.Selector):
585
608
 
586
609
  if deepest_node is not None:
587
610
  dict.update(deepest_node, {"covered": False})
588
-
589
-
kea2/utils.py CHANGED
@@ -5,7 +5,7 @@ import time
5
5
 
6
6
  from pathlib import Path
7
7
  from functools import wraps
8
- from typing import Callable, Dict, Optional
8
+ from typing import Callable, Dict, Optional, Union
9
9
 
10
10
 
11
11
  def singleton(cls):
@@ -70,13 +70,41 @@ class TimeStamp:
70
70
  import datetime
71
71
  cls.time_stamp = datetime.datetime.now().strftime('%Y%m%d%H_%M%S%f')
72
72
  return cls.time_stamp
73
+
74
+ def getCurrentTimeStamp(cls):
75
+ import datetime
76
+ return datetime.datetime.now().strftime('%Y%m%d%H_%M%S%f')
73
77
 
74
78
 
75
79
  from uiautomator2 import Device
76
80
  d = Device
77
81
 
78
82
 
83
+ _CUSTOM_PROJECT_ROOT: Optional[Path] = None
84
+
85
+
86
+ def setCustomProjectRoot(configs_path: Optional[Union[str, Path]]):
87
+ """
88
+ Set a custom project root directory (containing the configs directory). Passing None can restore the default behavior.
89
+ """
90
+ global _CUSTOM_PROJECT_ROOT
91
+
92
+ if configs_path is None:
93
+ _CUSTOM_PROJECT_ROOT = None
94
+ return
95
+
96
+ candidate = Path(configs_path).expanduser()
97
+ if candidate.name == "configs":
98
+ candidate = candidate.parent
99
+
100
+ candidate = candidate.resolve()
101
+ _CUSTOM_PROJECT_ROOT = candidate
102
+
103
+
79
104
  def getProjectRoot():
105
+ if _CUSTOM_PROJECT_ROOT:
106
+ return _CUSTOM_PROJECT_ROOT
107
+
80
108
  root = Path(Path.cwd().anchor)
81
109
  cur_dir = Path.absolute(Path(os.curdir))
82
110
  while not os.path.isdir(cur_dir / "configs"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 1.0.2
3
+ Version: 1.0.4
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
@@ -26,8 +26,11 @@ Dynamic: license-file
26
26
 
27
27
  Please contact Xixian Liang at [xixian@stu.ecnu.edu.cn](xixian@stu.ecnu.edu.cn) with your Wechat ID / QR code to be invited to the WeChat discussion group. Of course, we are also ready on GitHub to answer your questions/feedback.
28
28
 
29
- ### Github repo link
30
- [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
29
+ #### Github repo [https://github.com/ecnusse/Kea2](https://github.com/ecnusse/Kea2)
30
+ #### Gitee mirror [https://gitee.com/XixianLiang/Kea2](https://gitee.com/XixianLiang/Kea2)
31
+
32
+
33
+
31
34
 
32
35
  ### [点击此处:查看中文文档](README_cn.md)
33
36
 
@@ -43,7 +46,7 @@ Please contact Xixian Liang at [xixian@stu.ecnu.edu.cn](xixian@stu.ecnu.edu.cn)
43
46
 
44
47
  Kea2 is an easy-to-use tool for fuzzing mobile apps. Its key *novelty* is able to fuse automated UI testing with scripts (usually written by human), thus empowering automated UI testing with human intelligence for effectively finding *crashing bugs* as well as *non-crashing functional (logic) bugs*.
45
48
 
46
- Kea2 is currently built on top of [Fastbot](https://github.com/bytedance/Fastbot_Android), *an industrial-strength automated UI testing tool*, and [uiautomator2](https://github.com/openatx/uiautomator2), *an easy-to-use and stable Android automation library*.
49
+ Kea2 is currently built on top of [Fastbot](https://github.com/ecnusse/Fastbot_Android) 3.0 (a modified/enhanced version of the original [FastBot](https://github.com/bytedance/Fastbot_Android) 2.0), *an industrial-strength automated UI testing tool from ByteDance*, and [uiautomator2](https://github.com/openatx/uiautomator2), *an easy-to-use and stable Android automation library*.
47
50
  Kea2 currently targets [Android](https://en.wikipedia.org/wiki/Android_(operating_system)) apps.
48
51
 
49
52
  ## Novelty & Important features
@@ -90,7 +93,7 @@ Please let us know and willing to hear your feedback/questions if you are also u
90
93
  Kea2 currently works with:
91
94
  - [unittest](https://docs.python.org/3/library/unittest.html) as the testing framework to manage the scripts;
92
95
  - [uiautomator2](https://github.com/openatx/uiautomator2) as the UI test driver;
93
- - [Fastbot](https://github.com/bytedance/Fastbot_Android) as the backend automated UI testing tool.
96
+ - [Fastbot](https://github.com/bytedance/Fastbot_Android) as the backend automated UI testing tool.
94
97
 
95
98
  In the future, Kea2 will be extended to support
96
99
  - [pytest](https://docs.pytest.org/en/stable/), another popular python testing framework;
@@ -208,7 +211,7 @@ You can find the full example in script `quicktest.py`, and run this script with
208
211
 
209
212
  ```bash
210
213
  # Launch Kea2 and load one single script quicktest.py.
211
- kea2 run -p it.feio.android.omninotes.alpha --agent u2 --running-minutes 10 --throttle 200 --driver-name d propertytest discover -p quicktest.py
214
+ kea2 run -p it.feio.android.omninotes.alpha --running-minutes 10 --throttle 200 --driver-name d propertytest discover -p quicktest.py
212
215
  ```
213
216
 
214
217
  ## Feature 3(运行增强版Fastbot:加入自动断言)
@@ -243,7 +246,8 @@ For the preceding always-holding property, we can write the following script to
243
246
  )
244
247
  def test_input_box(self):
245
248
 
246
- # genenerate a random non-empty string (this is also property-based testing by feeding random text inputs!)
249
+ # genenerate a random non-empty string (this is also property-based testing
250
+ # by feeding random text inputs!)
247
251
  from hypothesis.strategies import text, ascii_letters
248
252
  random_str = text(alphabet=ascii_letters).example()
249
253
 
@@ -261,9 +265,7 @@ For the preceding always-holding property, we can write the following script to
261
265
 
262
266
  You can run this example by using the similar command line in Feature 2.
263
267
 
264
- ## Feature 4 (Experimental feature, 实验中,脚本与遍历的混合测试)
265
-
266
- > This feature is still under development. We are looking forward to your feedback! Contact us if you're interested in this feature.
268
+ ## Feature 4(兼容已有脚本:通过前置脚本步骤到达特定层次)
267
269
 
268
270
  Kea2 supports reusing existing Ui test Scripts. We are inspired by the idea that: *The existing Ui test scripts usually cover important app functionalities and can reach deep app states. Thus, they can be used as good "guiding scripts" to drive Fastbot to explore important and deep app states.*
269
271
 
@@ -271,22 +273,54 @@ For example, you may already have some existing Ui test scripts "login and add a
271
273
 
272
274
  ### Example
273
275
 
274
- See [guide_scripts.py](guide_scripts.py) for a full example.
276
+ Here are four example scripts in hybridetest_examples, each corresponding to different forms of user scripts, showing you how to launch kea2 in the existing code.
275
277
 
276
- By the decorator `@interruptable`, you can mark the testcase as "interruptable" so that Kea2 can recognize this script and launch fuzzing test after it returns.
278
+ Specifically:
277
279
 
278
- Since the state of app is probably unpredicted after fuzzing tests, Kea2 provides a `common_teardown` function to clean up the environment between previous script and next script. The function can be manually specified in `configs/teardown.py`.
280
+ * [u2_unittest_example.py](hybridtest_examples\u2_unittest_example.py) is a u2 script organized with unittest.
281
+ * [u2_pytest_example.py](hybridtest_examples\u2_pytest_example.py) is a u2 script organized with pytest.
282
+ * [appium_unittest_example.py](hybridtest_examples\appium_unittest_example.py) is an appium script organized with unittest.
283
+ * [appium_pytest_example.py](hybridtest_examples\appium_pytest_example.py) is an appium script organized with pytest.
279
284
 
280
- You can find the full example in `guide_scripts.py`, `property_omninotes.py` and `configs/teardown.py` . You can run one of the following commands:
285
+ Some notes:
281
286
 
282
- ```bash
283
- # Guide with guide_scripts.py and launch fuzzing test after every script.
284
- kea2 run -p it.feio.android.omninotes.alpha --agent u2 --running-minutes 10 --throttle 500 --max-step 15 --driver-name d unittest discover -p guide_scripts.py
287
+ 1. You can control whether to execute the kea2-related code you have written by modifying the condition of 'if'. This allows you to easily enable or disable kea2 operations in the same script. Here we use environment variable as an example.
288
+ 2. Since kea2 is driven by u2, if an appium-written script wants to launch kea2, it is necessary to first close the appium session. Remember to configure the parameter `"noReset": True` in `desired_caps` to avoid resetting the application when closing the session.
289
+ 3. You need to insert the following code template into your existing test cases: Here, you can add your own hook logic in the commented sections, including starting or stopping the appium session, cleaning up instances, etc. This depends on how you want to design the setup and teardown. Apart from that, you only need to configure the `option` parameter and `configs_path` parameter(where your directory `configs` located, btw, `configs`'s location dependon where you executed `kea2 init`), then pass it to the `run_kea2_testing` function.
285
290
 
286
- # Guide with guide_scripts.py and launch fuzzing test after every script(check properties during fuzzing).
287
- kea2 run -p it.feio.android.omninotes.alpha --agent u2 --running-minutes 10 --throttle 500 --max-step 15 --driver-name d unittest discover -p guide_scripts.py propertytest discover -p quickstart2.py
291
+ ```python
292
+ from kea2 import Kea2Tester, Options, U2Driver
293
+
294
+ if os.environ.get('KEA2_HYBRID_MODE', '').lower() == 'true':
295
+ '''
296
+ Note: The if condition here can be modified as needed according to the actual
297
+ situation of the project, the form of environment variables is just an example.
298
+ '''
299
+
300
+ # close your driver session etc. here
301
+ # ...
302
+
303
+ tester = Kea2Tester()
304
+ result = self.tester.run_kea2_testing(
305
+ Options(
306
+ driverName="d",
307
+ packageNames=[PACKAGE_NAME],
308
+ propertytest_args=["discover", "-p", "Omninotes_Sample.py"],
309
+ serial=DEVICE_SERIAL,
310
+ running_mins=2,
311
+ maxStep=20
312
+ ),
313
+ configs_path = None # Default, if your configs folder is located in the root directory, miss this.
314
+ )
315
+
316
+ # restart your driver session or clean instance here
317
+ # ...
318
+
319
+ return # this make your following steps of this testcase not work
288
320
  ```
289
321
 
322
+
323
+
290
324
  ## Test Reports(测试报告)
291
325
 
292
326
  Kea2 automatically generates a HTML test report after each testing session. You can find the report in `output/` under your working directory.
@@ -379,7 +413,7 @@ kea2做了什么:
379
413
 
380
414
  > Guided, Stochastic Model-Based GUI Testing of Android Apps. ESEC/FSE 2017. [pdf](https://dl.acm.org/doi/10.1145/3106237.3106298)
381
415
 
382
- ### Maintainers/Contributors
416
+ ## Maintainers/Contributors
383
417
 
384
418
  Kea2 has been actively developed and maintained by the people in [ecnusse](https://github.com/ecnusse):
385
419
 
@@ -397,7 +431,13 @@ Kea2 has been actively developed and maintained by the people in [ecnusse](https
397
431
 
398
432
  Kea2 has also received many valuable insights, advices, feedbacks and lessons shared by several industrial people from Bytedance ([Zhao Zhang](https://github.com/zhangzhao4444), Yuhui Su from the Fastbot team), OPay (Tiesong Liu), WeChat (Haochuan Lu, Yuetang Deng), Huawei, Xiaomi and etc. Kudos!
399
433
 
400
- ### Star History
434
+ ### Become a Contributor!
435
+
436
+ Kea2 is an open-source project and we are calling for more contributors to join us!
437
+
438
+ See [Developer guide](DEVELOP.md) for more details.
439
+
440
+ ## Star History
401
441
 
402
442
  [![Star History Chart](https://api.star-history.com/svg?repos=ecnusse/Kea2&type=Date)](https://www.star-history.com/#ecnusse/Kea2&Date)
403
443
 
@@ -1,21 +1,22 @@
1
- kea2/__init__.py,sha256=Vqf9XZkOau0Kz59tI4OJRRIOG1AnL45ZUuPrri99gFg,123
1
+ kea2/__init__.py,sha256=pwPZ2yYzws2pZrAtjczbG-_R1iVeY-_azD73DgWhe_U,187
2
2
  kea2/absDriver.py,sha256=NzmsLs1Ojz-yEXctGAqj7aKBwAQW19zd83l65RABCe8,1288
3
3
  kea2/adbUtils.py,sha256=pSmVlXEu3edfGL56Ag2mLCX9yIMHephnsUXzXUUEwNU,19904
4
- kea2/cli.py,sha256=KufFfXf-hkoaFeCJSvUTa2RbkTAWu4SuF9fp44E_SZ4,6173
5
- kea2/fastbotManager.py,sha256=Q1__QH0RbWw5QWTEW_QWydHaUf4xbBu2BTCu6XNYUFA,8940
6
- kea2/keaUtils.py,sha256=vUFeADftbv0DO8Mw4jeF2lQl0WIV27yQVCr00SQZPsY,36772
4
+ kea2/cli.py,sha256=kevMd2afcmvNEzQ8s8r84-gh01ic7jjalcGVAlch6SA,6206
5
+ kea2/fastbotManager.py,sha256=cgA4G26I5bNOrgOpMQvgYfD4MZTP1TpNuh0RG7euaDA,9057
6
+ kea2/kea2_api.py,sha256=MIpuR_HjVUhElhR9uNMCabTFc5VC463WXtZS9dKR_1c,6349
7
+ kea2/keaUtils.py,sha256=GeDVZ6SD44N1017W2vhxvvdDsFiXABL0q78YA0txja4,37251
7
8
  kea2/kea_launcher.py,sha256=6bGaO2eNSoFFLf6vSA1t1OTIPtGadUAZpYf3RS-pcik,9598
8
- kea2/logWatcher.py,sha256=cThOZ_awbJZh1wjeT6rxvdG_cDZSuzpaJmcYv5-OjkI,2594
9
+ kea2/logWatcher.py,sha256=KfdDIZjeQzv5CV-o5TlWU7WBTOOCzshElYTPk6BnJ0Y,2599
9
10
  kea2/mixin.py,sha256=2Z9c7BfI6Z-K970sK8FFzKeld0bFPDQWhmh9uuDy4BI,819
10
11
  kea2/resultSyncer.py,sha256=ZjtQfH1vY-u30EDhrJPfj_SNmGZC67uwL-oZCvekB5Q,2434
11
- kea2/u2Driver.py,sha256=SzkS9gRocKNBhsDl4BYQNy0X68p7EDupEpxUycRSmNc,20638
12
- kea2/utils.py,sha256=XNAEoF7DBru7BFyScbufZWFhGBdZ88EeL7jO0T8Ugik,4424
12
+ kea2/u2Driver.py,sha256=6GKMXjTiMW6PP5oYd9j1-TXQO4irAvPA5ohVeiayEtM,21252
13
+ kea2/utils.py,sha256=l0id-y8FKWxtAXR42RpIyLLPUOizlDWLHu07MofxyZY,5191
13
14
  kea2/version_manager.py,sha256=LGSD3OqgrI4LAwLDSa2G4WRVcDppY92s1SjlCMqsQ6c,3196
14
15
  kea2/assets/config_version.json,sha256=dYZRNo17hQq2SV45xxRCwDmykhsWWYI__w7vZXMHvXc,377
15
16
  kea2/assets/fastbot-thirdpart.jar,sha256=0SZ_OoZFWDGMnazgXKceHgKvXdUDoIa3Gb2bcifaikk,85664
16
17
  kea2/assets/framework.jar,sha256=rTluOJJKj2DFwh7ascXso1udYdWv00BxBwSQ3Vmv-fw,1149240
17
18
  kea2/assets/kea2-thirdpart.jar,sha256=HYdtG2gqDLuLb72dpK3lX-Y6QUNTrJ-bfQopU5aWpfo,359346
18
- kea2/assets/monkeyq.jar,sha256=9O7KprOjS0CHAINwvJSk1CL18zD7ylJlWt6uYitPdSE,114148
19
+ kea2/assets/monkeyq.jar,sha256=Ew0tliCWqoo88VF84JPrzp4M9Qni7SEAomjqZAgY5iE,114874
19
20
  kea2/assets/quicktest.py,sha256=FM3dlWpkCmeB1dGr2_zV1VcdLnkhsKYgdwyKyEg1378,4109
20
21
  kea2/assets/fastbot_configs/abl.strings,sha256=Rn8_YEbVGOJqndIY_-kWnR5NaoFI-cuB-ij10Ddhl90,75
21
22
  kea2/assets/fastbot_configs/awl.strings,sha256=-j4980GoWQxGOM9ijAwXPQmziCwFBZJFmuiv2tOEaYI,101
@@ -26,20 +27,20 @@ kea2/assets/fastbot_configs/max.strings,sha256=k82GAAZZG7KbDI7bk7DUklp41WJJO7j-j
26
27
  kea2/assets/fastbot_configs/max.tree.pruning,sha256=dm0oesN75FFbVSkV7STDUmrNMpQUBEuO7Uymt6nEkBc,629
27
28
  kea2/assets/fastbot_configs/teardown.py,sha256=dW6xHzozh2vXTB1qfCxAlT0xcv5kFwzX7kW1FmLfNA0,363
28
29
  kea2/assets/fastbot_configs/widget.block.py,sha256=r9Njm2xSBls3GMDIHTPMdmlFRiOjJWuVsq2KiieWFXA,1272
29
- kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so,sha256=tAFrG73pJi7XakRxSp9CavPHbexKvhC9NOMdt48IEuM,2006872
30
- kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so,sha256=UV8bhaiPoPKdd3q0vj3kSZqPR9anllai_tz_2QkMMbQ,1335372
31
- kea2/assets/fastbot_libs/x86/libfastbot_native.so,sha256=k-aw1gEXRWMKZRNHIggKNuZy0wC1y2BnveJGEIO6rbo,2036856
32
- kea2/assets/fastbot_libs/x86_64/libfastbot_native.so,sha256=tiofhlf4uMQcU5WAvrdLgTBME0lb83hVUoGtTwxmE8A,2121416
30
+ kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so,sha256=gh19xaSrZsEzDZrNriM9fQ2WO8infYHoNgVt4cVelnM,2011128
31
+ kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so,sha256=Z96EQOvoihtiLhWO1ca_EZ0vyeBPSMFLHYAgvjWTl-8,1338676
32
+ kea2/assets/fastbot_libs/x86/libfastbot_native.so,sha256=wC9wGxx6JozPaEoxRXZI4aMQ-HZ2mvM63Loyog3V9Qs,2042160
33
+ kea2/assets/fastbot_libs/x86_64/libfastbot_native.so,sha256=D2BBzBt5Tk-RwX38YL45OiGKKf2cZVFw_Vbr6WWJn68,2126440
33
34
  kea2/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- kea2/report/bug_report_generator.py,sha256=preCSHX1pUOqsPQmf5GJbkQROrNrFMH898a2Z9FMedc,30854
35
+ kea2/report/bug_report_generator.py,sha256=PUh3YACRhpQwgDScCQGOSlOU2V6cakBRL-XSRq4vcck,30877
35
36
  kea2/report/mixin.py,sha256=XtWHOW4wF3lI41PRxdRpq3qxnmaGOAy32TZ0aSLCuvI,18659
36
- kea2/report/report_merger.py,sha256=9x8mjOV9Lnf6sSCNL7oiiCeRk0-KmuiqdwAxUDfRGtc,27057
37
+ kea2/report/report_merger.py,sha256=U_arfWBtjkaqEETJLhIvD9UTW8-XohBLwunn4GNgSNQ,30800
37
38
  kea2/report/utils.py,sha256=r-oPtqbSDo8X0-V7zoIrz49TJ2-w-W444DS0de67mtk,344
38
39
  kea2/report/templates/bug_report_template.html,sha256=18Gpk8VPeO7TKN-_SEeDHG-XDiujlnA8PBvmaKGrbes,159182
39
40
  kea2/report/templates/merged_bug_report_template.html,sha256=iQHDW_XNVXfdAArPioELsgBcj1nAj9mmklqSVXGDQjQ,146912
40
- kea2_python-1.0.2.dist-info/licenses/LICENSE,sha256=nM9PPjcsXVo5SzNsjRqWgA-gdJlwqZZcRDSC6Qf6bVE,2034
41
- kea2_python-1.0.2.dist-info/METADATA,sha256=vga4APabMF9sVm_VkV_iuAaUz46s1Ki-LfLV1sz7gDs,23856
42
- kea2_python-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
- kea2_python-1.0.2.dist-info/entry_points.txt,sha256=mFX06TyxXiUAJQ6JZn8QHzfn8n5R8_KJ5W-pTm_fRCA,39
44
- kea2_python-1.0.2.dist-info/top_level.txt,sha256=TsgNH4PQoNOVhegpO7AcjutMVWp6Z4KDL1pBH9FnMmk,5
45
- kea2_python-1.0.2.dist-info/RECORD,,
41
+ kea2_python-1.0.4.dist-info/licenses/LICENSE,sha256=nM9PPjcsXVo5SzNsjRqWgA-gdJlwqZZcRDSC6Qf6bVE,2034
42
+ kea2_python-1.0.4.dist-info/METADATA,sha256=B6H3sT3uGlVGn6OlprCfM_GyE69SxC9JLjT0iDrxgDQ,25631
43
+ kea2_python-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
44
+ kea2_python-1.0.4.dist-info/entry_points.txt,sha256=mFX06TyxXiUAJQ6JZn8QHzfn8n5R8_KJ5W-pTm_fRCA,39
45
+ kea2_python-1.0.4.dist-info/top_level.txt,sha256=TsgNH4PQoNOVhegpO7AcjutMVWp6Z4KDL1pBH9FnMmk,5
46
+ kea2_python-1.0.4.dist-info/RECORD,,