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 +3 -1
- 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/monkeyq.jar +0 -0
- kea2/cli.py +6 -7
- kea2/fastbotManager.py +53 -51
- kea2/kea2_api.py +166 -0
- kea2/keaUtils.py +37 -20
- kea2/logWatcher.py +1 -1
- kea2/report/bug_report_generator.py +1 -1
- kea2/report/report_merger.py +91 -6
- kea2/u2Driver.py +86 -65
- kea2/utils.py +29 -1
- {kea2_python-1.0.2.dist-info → kea2_python-1.0.4.dist-info}/METADATA +61 -21
- {kea2_python-1.0.2.dist-info → kea2_python-1.0.4.dist-info}/RECORD +21 -20
- {kea2_python-1.0.2.dist-info → kea2_python-1.0.4.dist-info}/WHEEL +0 -0
- {kea2_python-1.0.2.dist-info → kea2_python-1.0.4.dist-info}/entry_points.txt +0 -0
- {kea2_python-1.0.2.dist-info → kea2_python-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {kea2_python-1.0.2.dist-info → kea2_python-1.0.4.dist-info}/top_level.txt +0 -0
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
201
|
+
if char in STAMP:
|
|
201
202
|
raise ValueError(
|
|
202
|
-
f"char: `{char}` is illegal in --log-stamp. current 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,
|
|
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
|
|
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
|
-
|
|
777
|
-
|
|
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
|
-
|
|
952
|
-
self.options.Driver
|
|
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
|
@@ -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
|
kea2/report/report_merger.py
CHANGED
|
@@ -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
|
-
|
|
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) ->
|
|
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
|
|
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
|
|
14
|
-
from .utils import
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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) -> '
|
|
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) -> '
|
|
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]) -> '
|
|
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) -> "
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
30
|
-
[https://
|
|
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 --
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
278
|
+
Specifically:
|
|
277
279
|
|
|
278
|
-
|
|
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
|
-
|
|
285
|
+
Some notes:
|
|
281
286
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
kea2
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
[](https://www.star-history.com/#ecnusse/Kea2&Date)
|
|
403
443
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
kea2/__init__.py,sha256=
|
|
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=
|
|
5
|
-
kea2/fastbotManager.py,sha256=
|
|
6
|
-
kea2/
|
|
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=
|
|
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=
|
|
12
|
-
kea2/utils.py,sha256=
|
|
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=
|
|
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=
|
|
30
|
-
kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so,sha256=
|
|
31
|
-
kea2/assets/fastbot_libs/x86/libfastbot_native.so,sha256=
|
|
32
|
-
kea2/assets/fastbot_libs/x86_64/libfastbot_native.so,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
41
|
-
kea2_python-1.0.
|
|
42
|
-
kea2_python-1.0.
|
|
43
|
-
kea2_python-1.0.
|
|
44
|
-
kea2_python-1.0.
|
|
45
|
-
kea2_python-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|