Kea2-python 1.1.0b1__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.
Files changed (49) hide show
  1. kea2/__init__.py +8 -0
  2. kea2/absDriver.py +56 -0
  3. kea2/adbUtils.py +554 -0
  4. kea2/assets/config_version.json +16 -0
  5. kea2/assets/fastbot-thirdpart.jar +0 -0
  6. kea2/assets/fastbot_configs/abl.strings +2 -0
  7. kea2/assets/fastbot_configs/awl.strings +3 -0
  8. kea2/assets/fastbot_configs/max.config +7 -0
  9. kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
  10. kea2/assets/fastbot_configs/max.schema.strings +1 -0
  11. kea2/assets/fastbot_configs/max.strings +3 -0
  12. kea2/assets/fastbot_configs/max.tree.pruning +27 -0
  13. kea2/assets/fastbot_configs/teardown.py +18 -0
  14. kea2/assets/fastbot_configs/widget.block.py +38 -0
  15. kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  16. kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  17. kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  18. kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  19. kea2/assets/framework.jar +0 -0
  20. kea2/assets/kea2-thirdpart.jar +0 -0
  21. kea2/assets/monkeyq.jar +0 -0
  22. kea2/assets/quicktest.py +126 -0
  23. kea2/cli.py +216 -0
  24. kea2/fastbotManager.py +269 -0
  25. kea2/kea2_api.py +166 -0
  26. kea2/keaUtils.py +926 -0
  27. kea2/kea_launcher.py +299 -0
  28. kea2/logWatcher.py +92 -0
  29. kea2/mixin.py +0 -0
  30. kea2/report/__init__.py +0 -0
  31. kea2/report/bug_report_generator.py +879 -0
  32. kea2/report/mixin.py +496 -0
  33. kea2/report/report_merger.py +1066 -0
  34. kea2/report/templates/bug_report_template.html +4028 -0
  35. kea2/report/templates/merged_bug_report_template.html +3602 -0
  36. kea2/report/utils.py +10 -0
  37. kea2/result.py +257 -0
  38. kea2/resultSyncer.py +65 -0
  39. kea2/state.py +22 -0
  40. kea2/typedefs.py +32 -0
  41. kea2/u2Driver.py +612 -0
  42. kea2/utils.py +192 -0
  43. kea2/version_manager.py +102 -0
  44. kea2_python-1.1.0b1.dist-info/METADATA +447 -0
  45. kea2_python-1.1.0b1.dist-info/RECORD +49 -0
  46. kea2_python-1.1.0b1.dist-info/WHEEL +5 -0
  47. kea2_python-1.1.0b1.dist-info/entry_points.txt +2 -0
  48. kea2_python-1.1.0b1.dist-info/licenses/LICENSE +16 -0
  49. kea2_python-1.1.0b1.dist-info/top_level.txt +1 -0
kea2/fastbotManager.py ADDED
@@ -0,0 +1,269 @@
1
+ import itertools
2
+ import requests
3
+
4
+ from time import sleep
5
+ from dataclasses import asdict
6
+ from pathlib import Path
7
+
8
+ from retry import retry
9
+ from retry.api import retry_call
10
+ from uiautomator2.core import HTTPResponse, _http_request
11
+ from packaging.version import parse as parse_version
12
+
13
+ from .utils import getLogger, getProjectRoot
14
+ from .adbUtils import ADBDevice, ADBStreamShell_V2
15
+
16
+
17
+ from typing import IO, TYPE_CHECKING, Dict
18
+ if TYPE_CHECKING:
19
+ from .keaUtils import Options
20
+ from .typedefs import PropertyExecutionInfo
21
+
22
+
23
+ logger = getLogger(__name__)
24
+
25
+
26
+ class FastbotManager:
27
+ def __init__(self, options: "Options", log_file: str):
28
+ self.options:"Options" = options
29
+ self.log_file: str = log_file
30
+ self.port = None
31
+ self.thread = None
32
+ self._device_output_dir = None
33
+ ADBDevice.setDevice(options.serial, options.transport_id)
34
+ self.dev = ADBDevice()
35
+ self.android_release = parse_version(self.dev.getprop("ro.build.version.release"))
36
+ self.executed_prop = False
37
+
38
+ def _activateFastbot(self) -> ADBStreamShell_V2:
39
+ """
40
+ activate fastbot.
41
+ :params: options: the running setting for fastbot
42
+ :params: port: the listening port for script driver
43
+ :return: the fastbot daemon thread
44
+ """
45
+
46
+ self._push_libs()
47
+ t = self._startFastbotService()
48
+ logger.info("Running Fastbot...")
49
+ return t
50
+
51
+ def check_alive(self):
52
+ """
53
+ check if the script driver and proxy server are alive.
54
+ """
55
+ def _check_alive_request():
56
+ _http_request(dev=self.dev, device_port=8090, method="GET", path="/ping")
57
+
58
+ try:
59
+ logger.info("Connecting to fastbot server...")
60
+ retry_call(_check_alive_request, tries=10, delay=2, logger=logger)
61
+ logger.info("Connected to fastbot server.")
62
+ except requests.ConnectionError:
63
+ raise RuntimeError("Failed to connect fastbot")
64
+
65
+ def request(self, method: str, path: str, data: Dict=None, timeout: int=10) -> HTTPResponse:
66
+ return _http_request(self.dev, 8090, method, path, data, timeout)
67
+
68
+ @retry(Exception, tries=2, delay=2)
69
+ def init(self, options: "Options", stamp):
70
+ post_data = {
71
+ "takeScreenshots": options.take_screenshots,
72
+ "preFailureScreenshots": options.pre_failure_screenshots,
73
+ "postFailureScreenshots": options.post_failure_screenshots,
74
+ "logStamp": stamp,
75
+ "deviceOutputRoot": options.device_output_root,
76
+ }
77
+ r = _http_request(
78
+ self.dev,
79
+ device_port=8090,
80
+ method="POST",
81
+ path="/init",
82
+ data=post_data
83
+ )
84
+ print(f"[INFO] Init fastbot: {post_data}", flush=True)
85
+ import re
86
+ self._device_output_dir = re.match(r"outputDir:(.+)", r.text).group(1)
87
+ print(f"[INFO] Fastbot initiated. outputDir: {r.text}", flush=True)
88
+
89
+ @retry(Exception, tries=2, delay=2)
90
+ def stepMonkey(self, monkeyStepInfo):
91
+ r = self.request(
92
+ method="POST",
93
+ path="/stepMonkey",
94
+ data=monkeyStepInfo
95
+ )
96
+ return r.json()["result"]
97
+
98
+ @retry(Exception, tries=2, delay=2)
99
+ def stopMonkey(self):
100
+ """
101
+ send a stop monkey request to the server.
102
+ """
103
+ r = self.request(
104
+ method="GET",
105
+ path="/stopMonkey",
106
+ )
107
+
108
+ print(f"[Server INFO] {r.text}", flush=True)
109
+
110
+ @retry(Exception, tries=2, delay=2)
111
+ def logScript(self, execution_info: "PropertyExecutionInfo"):
112
+ r = self.request(
113
+ method="POST",
114
+ path="/logScript",
115
+ data={
116
+ "propName": execution_info.propName,
117
+ "startStepsCount": execution_info.startStepsCount,
118
+ "kind": execution_info.kind,
119
+ "state": execution_info.state,
120
+ }
121
+ )
122
+ res = r.text
123
+ if res != "OK":
124
+ print(f"[ERROR] Error when logging script: {execution_info}", flush=True)
125
+
126
+ @retry(Exception, tries=2, delay=2)
127
+ def dumpHierarchy(self):
128
+ sleep(self.options.throttle / 1000)
129
+ r = self.request(
130
+ method="GET",
131
+ path="/dumpHierarchy",
132
+ )
133
+ return r.json()['result']
134
+
135
+ @retry(Exception, tries=2, delay=2)
136
+ def sendInfo(self, info: str):
137
+ r = self.request(
138
+ method="POST",
139
+ path="/sendInfo",
140
+ data=info
141
+ )
142
+
143
+ @property
144
+ def device_output_dir(self):
145
+ return self._device_output_dir
146
+
147
+ def _push_libs(self):
148
+ logger.info("Pushing Fastbot libraries to device...")
149
+ cur_dir = Path(__file__).parent
150
+ self.dev.sync.push(
151
+ Path.joinpath(cur_dir, "assets/monkeyq.jar"),
152
+ "/sdcard/monkeyq.jar"
153
+ )
154
+ self.dev.sync.push(
155
+ Path.joinpath(cur_dir, "assets/fastbot-thirdpart.jar"),
156
+ "/sdcard/fastbot-thirdpart.jar",
157
+ )
158
+ self.dev.sync.push(
159
+ Path.joinpath(cur_dir, "assets/kea2-thirdpart.jar"),
160
+ "/sdcard/kea2-thirdpart.jar",
161
+ )
162
+ self.dev.sync.push(
163
+ Path.joinpath(cur_dir, "assets/framework.jar"),
164
+ "/sdcard/framework.jar",
165
+ )
166
+ self.dev.sync.push(
167
+ Path.joinpath(cur_dir, "assets/fastbot_libs/arm64-v8a/libfastbot_native.so"),
168
+ "/data/local/tmp/arm64-v8a/libfastbot_native.so",
169
+ )
170
+ self.dev.sync.push(
171
+ Path.joinpath(cur_dir, "assets/fastbot_libs/armeabi-v7a/libfastbot_native.so"),
172
+ "/data/local/tmp/armeabi-v7a/libfastbot_native.so",
173
+ )
174
+ self.dev.sync.push(
175
+ Path.joinpath(cur_dir, "assets/fastbot_libs/x86/libfastbot_native.so"),
176
+ "/data/local/tmp/x86/libfastbot_native.so",
177
+ )
178
+ self.dev.sync.push(
179
+ Path.joinpath(cur_dir, "assets/fastbot_libs/x86_64/libfastbot_native.so"),
180
+ "/data/local/tmp/x86_64/libfastbot_native.so",
181
+ )
182
+
183
+ cwd = getProjectRoot()
184
+ whitelist = self.options.act_whitelist_file
185
+ blacklist = self.options.act_blacklist_file
186
+ if bool(whitelist) ^ bool(blacklist):
187
+ if whitelist:
188
+ file_to_push = cwd / 'configs' / 'awl.strings'
189
+ remote_path = whitelist
190
+ else:
191
+ file_to_push = cwd / 'configs' / 'abl.strings'
192
+ remote_path = blacklist
193
+
194
+ self.dev.sync.push(
195
+ file_to_push,
196
+ remote_path
197
+ )
198
+
199
+ def _startFastbotService(self) -> ADBStreamShell_V2:
200
+ shell_command = [
201
+ "CLASSPATH="
202
+ "/sdcard/monkeyq.jar:"
203
+ "/sdcard/framework.jar:"
204
+ "/sdcard/fastbot-thirdpart.jar:"
205
+ "/sdcard/kea2-thirdpart.jar",
206
+ "exec", "app_process",
207
+ "/system/bin", "com.android.commands.monkey.Monkey",
208
+ "--agent-u2" if self.options.agent == "u2" else "--agent",
209
+ "reuseq",
210
+ "--running-minutes", f"{self.options.running_mins}",
211
+ "--throttle", f"{self.options.throttle}",
212
+ "--bugreport",
213
+ "--output-directory", f"{self.options.device_output_root}/output_{self.options.log_stamp}",
214
+ ]
215
+
216
+ pkgs = itertools.chain.from_iterable(["-p", pkg] for pkg in self.options.packageNames)
217
+ shell_command.extend(pkgs)
218
+
219
+ if self.options.profile_period:
220
+ shell_command += ["--profile-period", f"{self.options.profile_period}"]
221
+
222
+ whitelist = self.options.act_whitelist_file
223
+ blacklist = self.options.act_blacklist_file
224
+ if bool(whitelist) ^ bool(blacklist):
225
+ if whitelist:
226
+ shell_command += ["--act-whitelist-file", f"{whitelist}"]
227
+ else:
228
+ shell_command += ["--act-blacklist-file", f"{blacklist}"]
229
+
230
+ shell_command += ["-v", "-v", "-v"]
231
+
232
+ if self.options.extra_args:
233
+ shell_command += self.options.extra_args
234
+
235
+ full_cmd = ["adb"] + (["-s", self.options.serial] if self.options.serial else []) + ["shell"] + shell_command
236
+
237
+
238
+ outfile = open(self.log_file, "w", encoding="utf-8", buffering=1)
239
+
240
+ logger.info("Options info: {}".format(asdict(self.options)))
241
+ logger.info("Launching fastbot with shell command:\n{}".format(" ".join(full_cmd)))
242
+ logger.info("Fastbot log will be saved to {}".format(outfile.name))
243
+
244
+ t = self.dev.stream_shell(shell_command, stdout=outfile, stderr=outfile)
245
+ return t
246
+
247
+ def close_on_exit(self, proc: ADBStreamShell_V2, f: IO):
248
+ self.return_code = proc.wait()
249
+ f.close()
250
+ if self.return_code != 0:
251
+ raise RuntimeError(f"Fastbot Error: Terminated with [code {self.return_code}] See {self.log_file} for details.")
252
+
253
+ def get_return_code(self):
254
+ if self.thread.is_running():
255
+ logger.info("Waiting for Fastbot to exit.")
256
+ return self.thread.wait()
257
+ return self.thread.poll() if self.android_release >= parse_version("7.0") else 0
258
+
259
+ def start(self):
260
+ # kill the fastbot process if runing.
261
+ self.dev.kill_proc("com.android.commands.monkey")
262
+ self.thread = self._activateFastbot()
263
+
264
+ def join(self):
265
+ self.thread.join()
266
+
267
+
268
+
269
+
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
+ }