Kea2-python 1.0.6b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Kea2-python might be problematic. Click here for more details.
- kea2/__init__.py +3 -0
- kea2/absDriver.py +56 -0
- kea2/adbUtils.py +554 -0
- kea2/assets/config_version.json +16 -0
- kea2/assets/fastbot-thirdpart.jar +0 -0
- kea2/assets/fastbot_configs/abl.strings +2 -0
- kea2/assets/fastbot_configs/awl.strings +3 -0
- kea2/assets/fastbot_configs/max.config +7 -0
- kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
- kea2/assets/fastbot_configs/max.schema.strings +1 -0
- kea2/assets/fastbot_configs/max.strings +3 -0
- kea2/assets/fastbot_configs/max.tree.pruning +27 -0
- kea2/assets/fastbot_configs/teardown.py +18 -0
- kea2/assets/fastbot_configs/widget.block.py +38 -0
- kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
- kea2/assets/framework.jar +0 -0
- kea2/assets/kea2-thirdpart.jar +0 -0
- kea2/assets/monkeyq.jar +0 -0
- kea2/assets/quicktest.py +126 -0
- kea2/cli.py +320 -0
- kea2/fastbotManager.py +267 -0
- kea2/fastbotx/ActivityTimes.py +52 -0
- kea2/fastbotx/ReuseEntry.py +74 -0
- kea2/fastbotx/ReuseModel.py +63 -0
- kea2/fastbotx/__init__.py +7 -0
- kea2/fbm_parser.py +871 -0
- kea2/fs_lock.py +131 -0
- kea2/kea2_api.py +166 -0
- kea2/keaUtils.py +1112 -0
- kea2/kea_launcher.py +319 -0
- kea2/logWatcher.py +92 -0
- kea2/mixin.py +22 -0
- kea2/report/__init__.py +0 -0
- kea2/report/bug_report_generator.py +793 -0
- kea2/report/mixin.py +482 -0
- kea2/report/report_merger.py +797 -0
- kea2/report/templates/bug_report_template.html +3876 -0
- kea2/report/templates/merged_bug_report_template.html +3333 -0
- kea2/report/utils.py +10 -0
- kea2/resultSyncer.py +65 -0
- kea2/u2Driver.py +610 -0
- kea2/utils.py +184 -0
- kea2/version_manager.py +102 -0
- kea2_python-1.0.6b0.dist-info/METADATA +447 -0
- kea2_python-1.0.6b0.dist-info/RECORD +52 -0
- kea2_python-1.0.6b0.dist-info/WHEEL +5 -0
- kea2_python-1.0.6b0.dist-info/entry_points.txt +2 -0
- kea2_python-1.0.6b0.dist-info/licenses/LICENSE +16 -0
- kea2_python-1.0.6b0.dist-info/top_level.txt +1 -0
kea2/fs_lock.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import errno
|
|
4
|
+
import random
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
class LockTimeoutError(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class FileLock:
|
|
11
|
+
"""Simple cross-platform exclusive lock with per-thread reentrancy.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
with FileLock(path, timeout=10.0, poll_interval=0.1):
|
|
15
|
+
# exclusive section
|
|
16
|
+
|
|
17
|
+
Implementation:
|
|
18
|
+
- If `portalocker` is available it will use that for advisory file locking.
|
|
19
|
+
- Otherwise falls back to an atomic mkdir-based lock (create a directory '<path>.lockdir').
|
|
20
|
+
- Reentrancy: a thread that already holds the lock can re-enter without blocking.
|
|
21
|
+
"""
|
|
22
|
+
# thread-local storage to track held locks: mapping lock_id -> {'count': int, 'lockfile': file obj or None}
|
|
23
|
+
_local = threading.local()
|
|
24
|
+
|
|
25
|
+
def __init__(self, path, timeout=10.0, poll_interval=0.1):
|
|
26
|
+
self.target = os.fspath(path)
|
|
27
|
+
self.timeout = float(timeout)
|
|
28
|
+
self.poll_interval = float(poll_interval)
|
|
29
|
+
self._use_portalocker = False
|
|
30
|
+
self._portalocker = None
|
|
31
|
+
self._lockfile = None
|
|
32
|
+
self._lockdir = None
|
|
33
|
+
try:
|
|
34
|
+
import portalocker # type: ignore
|
|
35
|
+
self._portalocker = portalocker
|
|
36
|
+
self._use_portalocker = True
|
|
37
|
+
except Exception:
|
|
38
|
+
self._portalocker = None
|
|
39
|
+
self._use_portalocker = False
|
|
40
|
+
# Normalize lock id
|
|
41
|
+
self._lock_id = os.path.abspath(self.target)
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
if not hasattr(FileLock._local, 'held'):
|
|
45
|
+
FileLock._local.held = {}
|
|
46
|
+
held = FileLock._local.held
|
|
47
|
+
# Reentrant: if we already hold this lock in this thread, increment counter and return
|
|
48
|
+
if self._lock_id in held:
|
|
49
|
+
held[self._lock_id]['count'] += 1
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
deadline = time.time() + self.timeout
|
|
53
|
+
if self._use_portalocker:
|
|
54
|
+
lock_path = self.target if os.path.isdir(self.target) else (self.target + '.lockfile')
|
|
55
|
+
parent = os.path.dirname(lock_path) or '.'
|
|
56
|
+
try:
|
|
57
|
+
os.makedirs(parent, exist_ok=True)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
# open lock file
|
|
61
|
+
lf = open(lock_path, 'a+b')
|
|
62
|
+
while True:
|
|
63
|
+
try:
|
|
64
|
+
self._portalocker.lock(lf, self._portalocker.LockFlags.EXCLUSIVE | self._portalocker.LockFlags.NON_BLOCKING)
|
|
65
|
+
# record in thread-local
|
|
66
|
+
held[self._lock_id] = {'count': 1, 'lockfile': lf, 'lockdir': None}
|
|
67
|
+
return self
|
|
68
|
+
except Exception:
|
|
69
|
+
if time.time() > deadline:
|
|
70
|
+
try:
|
|
71
|
+
lf.close()
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
raise LockTimeoutError(f"Timeout acquiring portalocker on {lock_path}")
|
|
75
|
+
time.sleep(self.poll_interval + random.random() * 0.01)
|
|
76
|
+
else:
|
|
77
|
+
lockdir = (self.target + '.lockdir') if not str(self.target).endswith('.lockdir') else self.target
|
|
78
|
+
while True:
|
|
79
|
+
try:
|
|
80
|
+
os.mkdir(lockdir)
|
|
81
|
+
try:
|
|
82
|
+
with open(os.path.join(lockdir, 'owner'), 'w') as f:
|
|
83
|
+
f.write(f"{os.getpid()}\n{time.time()}\n")
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
held[self._lock_id] = {'count': 1, 'lockfile': None, 'lockdir': lockdir}
|
|
87
|
+
return self
|
|
88
|
+
except OSError as e:
|
|
89
|
+
if e.errno != errno.EEXIST:
|
|
90
|
+
raise
|
|
91
|
+
if time.time() > deadline:
|
|
92
|
+
raise LockTimeoutError(f"Timeout acquiring mkdir lock on {lockdir}")
|
|
93
|
+
time.sleep(self.poll_interval + random.random() * 0.01)
|
|
94
|
+
|
|
95
|
+
def __exit__(self, exc_type, exc, tb):
|
|
96
|
+
held = getattr(FileLock._local, 'held', None)
|
|
97
|
+
if not held or self._lock_id not in held:
|
|
98
|
+
# nothing to release
|
|
99
|
+
return
|
|
100
|
+
entry = held[self._lock_id]
|
|
101
|
+
entry['count'] -= 1
|
|
102
|
+
if entry['count'] > 0:
|
|
103
|
+
return
|
|
104
|
+
# count reached zero: release underlying OS lock
|
|
105
|
+
if entry.get('lockfile'):
|
|
106
|
+
try:
|
|
107
|
+
self._portalocker.unlock(entry['lockfile'])
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
try:
|
|
111
|
+
entry['lockfile'].close()
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
else:
|
|
115
|
+
lockdir = entry.get('lockdir')
|
|
116
|
+
if lockdir:
|
|
117
|
+
try:
|
|
118
|
+
owner = os.path.join(lockdir, 'owner')
|
|
119
|
+
if os.path.exists(owner):
|
|
120
|
+
try:
|
|
121
|
+
os.remove(owner)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
os.rmdir(lockdir)
|
|
125
|
+
except Exception:
|
|
126
|
+
# best-effort cleanup only
|
|
127
|
+
pass
|
|
128
|
+
try:
|
|
129
|
+
del held[self._lock_id]
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
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
|
+
}
|