Kea2-python 1.1.3b0__tar.gz → 1.1.3b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/Kea2_python.egg-info/PKG-INFO +2 -1
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/Kea2_python.egg-info/SOURCES.txt +1 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/Kea2_python.egg-info/requires.txt +1 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/PKG-INFO +2 -1
- kea2_python-1.1.3b2/kea2/adbUtils.py +247 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
- kea2_python-1.1.3b2/kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
- kea2_python-1.1.3b2/kea2/assets/monkeyq.jar +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fbm_parser.py +19 -24
- kea2_python-1.1.3b2/kea2/fbm_plugin.py +98 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/keaUtils.py +2 -113
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/kea_launcher.py +12 -22
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/logWatcher.py +21 -6
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/bug_report_generator.py +9 -3
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/report_merger.py +11 -1
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/templates/bug_report_template.html +193 -71
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/templates/merged_bug_report_template.html +154 -69
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/widget_coverage.py +36 -26
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/u2Driver.py +0 -10
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/pyproject.toml +2 -2
- kea2_python-1.1.3b0/kea2/adbUtils.py +0 -554
- kea2_python-1.1.3b0/kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
- kea2_python-1.1.3b0/kea2/assets/monkeyq.jar +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/Kea2_python.egg-info/dependency_links.txt +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/Kea2_python.egg-info/entry_points.txt +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/Kea2_python.egg-info/top_level.txt +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/LICENSE +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/README.md +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/__init__.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/config_version.json +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot-thirdpart.jar +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/abl.strings +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/awl.strings +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.config +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.fuzzing.strings +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.schema.strings +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.strings +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.tree.pruning +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/teardown.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/widget.block.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/framework.jar +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/kea2-thirdpart.jar +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/quicktest.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/cli.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fastbotManager.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fastbotx/ActivityTimes.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fastbotx/ReuseEntry.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fastbotx/ReuseModel.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fastbotx/__init__.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/fs_lock.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/kea2_api.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/mixin.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/__init__.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/mixin.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/report/utils.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/result.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/resultSyncer.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/state.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/typedefs.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/utils.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/version_manager.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/setup.cfg +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/tests/test_u2Selector.py +0 -0
- {kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/tests/test_xpath.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Kea2-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3b2
|
|
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
|
|
@@ -10,6 +10,7 @@ Requires-Dist: rtree>=1.3.0
|
|
|
10
10
|
Requires-Dist: jinja2>=3.0.0
|
|
11
11
|
Requires-Dist: uiautomator2>=3.3.3
|
|
12
12
|
Requires-Dist: adbutils>=2.9.3
|
|
13
|
+
Requires-Dist: flatbuffers>=25.9.23
|
|
13
14
|
Requires-Dist: packaging>=25.0
|
|
14
15
|
Dynamic: license-file
|
|
15
16
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Kea2-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3b2
|
|
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
|
|
@@ -10,6 +10,7 @@ Requires-Dist: rtree>=1.3.0
|
|
|
10
10
|
Requires-Dist: jinja2>=3.0.0
|
|
11
11
|
Requires-Dist: uiautomator2>=3.3.3
|
|
12
12
|
Requires-Dist: adbutils>=2.9.3
|
|
13
|
+
Requires-Dist: flatbuffers>=25.9.23
|
|
13
14
|
Requires-Dist: packaging>=25.0
|
|
14
15
|
Dynamic: license-file
|
|
15
16
|
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
from typing import IO, Generator, Optional, List, Union, List, Optional, Set, Tuple
|
|
5
|
+
|
|
6
|
+
from adbutils import AdbDevice, adb
|
|
7
|
+
|
|
8
|
+
from .utils import getLogger
|
|
9
|
+
|
|
10
|
+
logger = getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ADBDevice(AdbDevice):
|
|
14
|
+
_instance = None
|
|
15
|
+
serial: Optional[str] = None
|
|
16
|
+
transport_id: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
def __new__(cls):
|
|
19
|
+
if cls._instance is None:
|
|
20
|
+
cls._instance = super().__new__(cls)
|
|
21
|
+
return cls._instance
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def setDevice(cls, serial: Optional[str] = None, transport_id: Optional[str] = None):
|
|
25
|
+
ADBDevice.serial = serial or ADBDevice.serial
|
|
26
|
+
ADBDevice.transport_id = transport_id or ADBDevice.transport_id
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> AdbDevice:
|
|
29
|
+
"""
|
|
30
|
+
Initializes the ADBDevice instance.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
device (str, optional): The device serial number. If None, it is resolved automatically when only one device is connected.
|
|
34
|
+
transport_id (str, optional): The transport ID for the device.
|
|
35
|
+
"""
|
|
36
|
+
if not ADBDevice.serial and not ADBDevice.transport_id:
|
|
37
|
+
devices = [d.serial for d in adb.list() if d.state == "device"]
|
|
38
|
+
if len(devices) > 1:
|
|
39
|
+
raise RuntimeError("Multiple devices connected. Please specify a device")
|
|
40
|
+
if len(devices) == 0:
|
|
41
|
+
raise RuntimeError("No device connected.")
|
|
42
|
+
ADBDevice.serial = devices[0]
|
|
43
|
+
super().__init__(client=adb, serial=ADBDevice.serial, transport_id=ADBDevice.transport_id)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def stream_shell(self) -> "StreamShell":
|
|
47
|
+
if "shell_v2" in self.get_features():
|
|
48
|
+
return ADBStreamShell_V2(session=self)
|
|
49
|
+
logger.warning("Using ADBStreamShell_V1. All output will be printed to stdout.")
|
|
50
|
+
return ADBStreamShell_V1(session=self)
|
|
51
|
+
|
|
52
|
+
def kill_proc(self, proc_name):
|
|
53
|
+
r = self.shell(f"ps -ef")
|
|
54
|
+
pids = [l for l in r.splitlines() if proc_name in l]
|
|
55
|
+
if pids:
|
|
56
|
+
logger.info(f"{proc_name} running, trying to kill it.")
|
|
57
|
+
pid = pids[0].split()[1]
|
|
58
|
+
self.shell(f"kill {pid}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StreamShell:
|
|
62
|
+
def __init__(self, session: "ADBDevice"):
|
|
63
|
+
self.dev: ADBDevice = session
|
|
64
|
+
self._thread: threading.Thread = None
|
|
65
|
+
self._exit_code = 255
|
|
66
|
+
self.stdout = sys.stdout
|
|
67
|
+
self.stderr = sys.stderr
|
|
68
|
+
self._finished = False
|
|
69
|
+
|
|
70
|
+
def __call__(self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
71
|
+
stderr: IO = None, timeout: Union[float, None] = None) -> "StreamShell":
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def _write_stdout(self, data: bytes, decode=True):
|
|
75
|
+
text = data.decode('utf-8', errors='ignore') if decode else data
|
|
76
|
+
self.stdout.write(text)
|
|
77
|
+
self.stdout.flush()
|
|
78
|
+
|
|
79
|
+
def _write_stderr(self, data: bytes, decode=True):
|
|
80
|
+
text = data.decode('utf-8', errors='ignore') if decode else data
|
|
81
|
+
self.stderr.write(text)
|
|
82
|
+
self.stderr.flush()
|
|
83
|
+
|
|
84
|
+
def wait(self):
|
|
85
|
+
""" Wait for the shell command to finish and return the exit code.
|
|
86
|
+
Returns:
|
|
87
|
+
int: The exit code of the shell command.
|
|
88
|
+
"""
|
|
89
|
+
if self._thread:
|
|
90
|
+
self._thread.join()
|
|
91
|
+
return self._exit_code
|
|
92
|
+
|
|
93
|
+
def is_running(self) -> bool:
|
|
94
|
+
""" Check if the shell command is still running.
|
|
95
|
+
Returns:
|
|
96
|
+
bool: True if the command is still running, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
return not self._finished and self._thread and self._thread.is_alive()
|
|
99
|
+
|
|
100
|
+
def poll(self):
|
|
101
|
+
"""
|
|
102
|
+
Check if the shell command is still running.
|
|
103
|
+
Returns:
|
|
104
|
+
int: The exit code if the command has finished, None otherwise.
|
|
105
|
+
"""
|
|
106
|
+
if self._thread and self._thread.is_alive():
|
|
107
|
+
return None
|
|
108
|
+
return self._exit_code
|
|
109
|
+
|
|
110
|
+
def join(self):
|
|
111
|
+
if self._thread and self._thread.is_alive():
|
|
112
|
+
self._thread.join()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ADBStreamShell_V1(StreamShell):
|
|
116
|
+
|
|
117
|
+
def __call__(
|
|
118
|
+
self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
119
|
+
stderr: IO = None, timeout: Union[float, None] = None
|
|
120
|
+
) -> "StreamShell":
|
|
121
|
+
return self.shell_v1(cmdargs, stdout, stderr, timeout)
|
|
122
|
+
|
|
123
|
+
def shell_v1(
|
|
124
|
+
self, cmdargs: Union[List[str], str],
|
|
125
|
+
stdout: IO = None, stderr: IO = None,
|
|
126
|
+
timeout: Union[float, None] = None
|
|
127
|
+
):
|
|
128
|
+
self._finished = False
|
|
129
|
+
self.stdout: IO = stdout if stdout else sys.stdout
|
|
130
|
+
self.stderr: IO = stderr if stderr else sys.stderr
|
|
131
|
+
|
|
132
|
+
cmd = " ".join(cmdargs) if isinstance(cmdargs, list) else cmdargs
|
|
133
|
+
self._generator = self._shell_v1(cmd, timeout)
|
|
134
|
+
self._thread = threading.Thread(target=self._process_output, daemon=True)
|
|
135
|
+
self._thread.start()
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _shell_v1(self, cmdargs: str, timeout: Optional[float] = None) -> Generator[Tuple[str, str], None, None]:
|
|
140
|
+
if not isinstance(cmdargs, str):
|
|
141
|
+
raise RuntimeError("_shell_v1 args must be str")
|
|
142
|
+
MAGIC = "X4EXIT:"
|
|
143
|
+
newcmd = cmdargs + f"; echo {MAGIC}$?"
|
|
144
|
+
with self.dev.open_transport(timeout=timeout) as c:
|
|
145
|
+
c.send_command(f"shell:{newcmd}")
|
|
146
|
+
c.check_okay()
|
|
147
|
+
with c.conn.makefile("r", encoding="utf-8") as f:
|
|
148
|
+
for line in f:
|
|
149
|
+
rindex = line.rfind(MAGIC)
|
|
150
|
+
if rindex == -1:
|
|
151
|
+
yield "output", line
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
yield "exit", line[rindex + len(MAGIC):]
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
def _process_output(self):
|
|
158
|
+
try:
|
|
159
|
+
for msg_type, data in self._generator:
|
|
160
|
+
|
|
161
|
+
if msg_type == 'output':
|
|
162
|
+
self._write_stdout(data, decode=False)
|
|
163
|
+
elif msg_type == 'exit':
|
|
164
|
+
# TODO : handle exit code properly
|
|
165
|
+
# self._exit_code = int(data.strip())
|
|
166
|
+
self._exit_code = 0
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"ADBStreamShell execution error: {e}")
|
|
171
|
+
self._exit_code = -1
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ADBStreamShell_V2(StreamShell):
|
|
175
|
+
def __init__(self, session: "ADBDevice"):
|
|
176
|
+
self.dev: ADBDevice = session
|
|
177
|
+
self._thread = None
|
|
178
|
+
self._exit_code = 255
|
|
179
|
+
|
|
180
|
+
def __call__(
|
|
181
|
+
self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
182
|
+
stderr: IO = None, timeout: Union[float, None] = None
|
|
183
|
+
) -> "StreamShell":
|
|
184
|
+
return self.shell_v2(cmdargs, stdout, stderr, timeout)
|
|
185
|
+
|
|
186
|
+
def shell_v2(
|
|
187
|
+
self, cmdargs: Union[List[str], str],
|
|
188
|
+
stdout: IO = None, stderr: IO = None,
|
|
189
|
+
timeout: Union[float, None] = None
|
|
190
|
+
):
|
|
191
|
+
""" Start a shell command on the device and stream its output.
|
|
192
|
+
Args:
|
|
193
|
+
cmdargs (Union[List[str], str]): The command to execute, either as a list of arguments or a single string.
|
|
194
|
+
stdout (IO, optional): The output stream for standard output. Defaults to sys.stdout.
|
|
195
|
+
stderr (IO, optional): The output stream for standard error. Defaults to sys.stderr.
|
|
196
|
+
timeout (Union[float, None], optional): Timeout for the command execution. Defaults to None.
|
|
197
|
+
Returns:
|
|
198
|
+
ADBStreamShell: An instance of ADBStreamShell that can be used to interact with the shell command.
|
|
199
|
+
"""
|
|
200
|
+
self._finished = False
|
|
201
|
+
self.stdout: IO = stdout if stdout else sys.stdout
|
|
202
|
+
self.stderr: IO = stderr if stderr else sys.stderr
|
|
203
|
+
|
|
204
|
+
cmd = " ".join(cmdargs) if isinstance(cmdargs, list) else cmdargs
|
|
205
|
+
self._generator = self._shell_v2(cmd, timeout)
|
|
206
|
+
self._thread = threading.Thread(target=self._process_output, daemon=True)
|
|
207
|
+
self._thread.start()
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def _process_output(self):
|
|
211
|
+
try:
|
|
212
|
+
for msg_type, data in self._generator:
|
|
213
|
+
|
|
214
|
+
if msg_type == 'stdout':
|
|
215
|
+
self._write_stdout(data)
|
|
216
|
+
elif msg_type == 'stderr':
|
|
217
|
+
self._write_stderr(data)
|
|
218
|
+
elif msg_type == 'exit':
|
|
219
|
+
self._exit_code = data
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
print(f"ADBStreamShell execution error: {e}")
|
|
224
|
+
self._exit_code = -1
|
|
225
|
+
|
|
226
|
+
def _shell_v2(self, cmd, timeout) -> Generator[Tuple[str, bytes], None, None]:
|
|
227
|
+
with self.dev.open_transport(timeout=timeout) as c:
|
|
228
|
+
c.send_command(f"shell,v2:{cmd}")
|
|
229
|
+
c.check_okay()
|
|
230
|
+
|
|
231
|
+
while True:
|
|
232
|
+
header = c.read_exact(5)
|
|
233
|
+
msg_id = header[0]
|
|
234
|
+
length = int.from_bytes(header[1:5], byteorder="little")
|
|
235
|
+
|
|
236
|
+
if length == 0:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
data = c.read_exact(length)
|
|
240
|
+
|
|
241
|
+
if msg_id == 1:
|
|
242
|
+
yield ('stdout', data)
|
|
243
|
+
elif msg_id == 2:
|
|
244
|
+
yield ('stderr', data)
|
|
245
|
+
elif msg_id == 3:
|
|
246
|
+
yield ('exit', data[0])
|
|
247
|
+
break
|
{kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so
RENAMED
|
Binary file
|
|
Binary file
|
|
Binary file
|
{kea2_python-1.1.3b0 → kea2_python-1.1.3b2}/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so
RENAMED
|
Binary file
|
|
Binary file
|
|
@@ -578,13 +578,12 @@ class FBMMerger:
|
|
|
578
578
|
Returns True on success (or if nothing to do), False on failure.
|
|
579
579
|
"""
|
|
580
580
|
try:
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return False
|
|
581
|
+
# Use upstream adbutils directly (avoids relying on removed wrapper helpers)
|
|
582
|
+
from adbutils import adb
|
|
583
|
+
dev = adb.device(device) if device else adb.device()
|
|
584
|
+
except Exception as e:
|
|
585
|
+
print("ADB utilities (adbutils) not available:", e)
|
|
586
|
+
return False
|
|
588
587
|
|
|
589
588
|
pc_dir = self._pc_dir
|
|
590
589
|
pc_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -597,9 +596,9 @@ class FBMMerger:
|
|
|
597
596
|
remote = self._remote_fbm_path(package_name)
|
|
598
597
|
try:
|
|
599
598
|
print(f"Attempting to pull {remote} to {pulled_tmp}")
|
|
600
|
-
|
|
599
|
+
dev.sync.pull(remote, str(pulled_tmp))
|
|
601
600
|
except Exception as e:
|
|
602
|
-
print(f"
|
|
601
|
+
print(f"dev.sync.pull failed for {remote}: {e}")
|
|
603
602
|
|
|
604
603
|
if not pulled_tmp.exists() or pulled_tmp.stat().st_size == 0:
|
|
605
604
|
print(f"No FBM on device for {package_name}, nothing merged to PC.")
|
|
@@ -617,7 +616,7 @@ class FBMMerger:
|
|
|
617
616
|
try:
|
|
618
617
|
# attempt to pull snapshot (may fail silently)
|
|
619
618
|
try:
|
|
620
|
-
|
|
619
|
+
dev.sync.pull(snapshot_remote, str(pulled_snap_tmp))
|
|
621
620
|
except Exception:
|
|
622
621
|
# snapshot may not exist on device; ignore error and proceed (treat as empty)
|
|
623
622
|
pass
|
|
@@ -676,25 +675,25 @@ class FBMMerger:
|
|
|
676
675
|
src = self._remote_fbm_path(package_name)
|
|
677
676
|
dst = snapshot_remote or f"/sdcard/fastbot_{package_name}.snapshot.fbm"
|
|
678
677
|
try:
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
return False
|
|
678
|
+
# Prefer using adbutils directly for shell commands
|
|
679
|
+
from adbutils import adb
|
|
680
|
+
dev = adb.device(device) if device else adb.device()
|
|
681
|
+
except Exception as e:
|
|
682
|
+
print("ADB utilities not available:", e)
|
|
683
|
+
return False
|
|
686
684
|
|
|
687
685
|
try:
|
|
688
686
|
print(f"Creating device snapshot: cp {src} {dst}")
|
|
689
|
-
|
|
687
|
+
# use device.shell (string) to avoid subprocess-specific flags like -t
|
|
688
|
+
dev.shell(f'cp "{src}" "{dst}"')
|
|
690
689
|
return True
|
|
691
690
|
except Exception as e:
|
|
692
691
|
print(f"adb shell cp failed ({e}), trying pull/push fallback")
|
|
693
692
|
# fallback: pull then push to dst
|
|
694
693
|
try:
|
|
695
694
|
pc_tmp = os.path.join(self._pc_dir, f"fastbot_{package_name}.snapshot.from_device.fbm")
|
|
696
|
-
|
|
697
|
-
|
|
695
|
+
dev.sync.pull(src, pc_tmp)
|
|
696
|
+
dev.sync.push(pc_tmp, dst)
|
|
698
697
|
try:
|
|
699
698
|
os.remove(pc_tmp)
|
|
700
699
|
except Exception:
|
|
@@ -865,7 +864,3 @@ class FBMMerger:
|
|
|
865
864
|
print(f"Timeout acquiring lock to merge/apply delta into {pc_fbm}")
|
|
866
865
|
return False
|
|
867
866
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import functools
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from retry import retry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .keaUtils import KeaTestRunner, Options
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .adbUtils import ADBDevice
|
|
12
|
+
from .utils import catchException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FBMSanapshotCreationError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@catchException("Error creating device FBM snapshots")
|
|
24
|
+
@retry(exceptions=FBMSanapshotCreationError, tries=3, delay=3)
|
|
25
|
+
def create_device_snapshots(options: "Options") -> None:
|
|
26
|
+
"""Create on-device snapshot copies of fastbot fbm files for configured packages.
|
|
27
|
+
|
|
28
|
+
Behavior:
|
|
29
|
+
- Only runs when options.download_fbm is truthy.
|
|
30
|
+
- Uses ADBDevice.shell (no subprocess) and retries cp up to max_retries.
|
|
31
|
+
- Logs errors and never raises to avoid blocking startup.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
for pkg in options.packageNames:
|
|
35
|
+
src = f"/sdcard/fastbot_{pkg}.fbm"
|
|
36
|
+
dst = f"/sdcard/fastbot_{pkg}.snapshot.fbm"
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Check src existence
|
|
40
|
+
check_cmd = f'test -f "{src}" && echo OK || echo NO'
|
|
41
|
+
check_src = ADBDevice().shell(check_cmd)
|
|
42
|
+
if not (isinstance(check_src, str) and "OK" in check_src):
|
|
43
|
+
print(f"Source FBM not found on device for package {pkg}: {src}. Skipping snapshot creation.", flush=True)
|
|
44
|
+
continue
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error(f"Failed to verify source FBM existence for {pkg}: {e}. Skipping.")
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
ADBDevice().shell(f'cp "{src}" "{dst}"')
|
|
50
|
+
|
|
51
|
+
# verify snapshot exists
|
|
52
|
+
verify_cmd = f'test -f "{dst}" && echo OK || echo NO'
|
|
53
|
+
r = ADBDevice().shell(verify_cmd)
|
|
54
|
+
if not "OK" in r:
|
|
55
|
+
raise FBMSanapshotCreationError("Failed to create ")
|
|
56
|
+
logger.info(f"Snapshot created on device for package {pkg}: {dst}", flush=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@catchException("Error finalizing and merging FBM deltas")
|
|
61
|
+
def finalize_and_merge(options: "Options"):
|
|
62
|
+
"""Pull device fbms, compute deltas and merge deltas into PC core fbm.
|
|
63
|
+
|
|
64
|
+
Uses kea2.fbm_parser.FBMMerger.pull_and_merge_to_pc for each package.
|
|
65
|
+
"""
|
|
66
|
+
from .fbm_parser import FBMMerger
|
|
67
|
+
|
|
68
|
+
merger = FBMMerger()
|
|
69
|
+
for pkg in options.packageNames:
|
|
70
|
+
logger.info(f"Finalizing FBM delta for package: {pkg}")
|
|
71
|
+
ok = merger.pull_and_merge_to_pc(pkg, device=options.serial, transport_id=options.transport_id)
|
|
72
|
+
if ok:
|
|
73
|
+
logger.info(f"Delta merge completed for package: {pkg}")
|
|
74
|
+
else:
|
|
75
|
+
logger.error(f"Delta merge reported failure for package: {pkg}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def merge_fbm(func):
|
|
79
|
+
"""Decorator for KeaTestRunner.run:
|
|
80
|
+
|
|
81
|
+
Function: Merge FBM in multi-device test run to accelerate fastbot model training.
|
|
82
|
+
|
|
83
|
+
The decorator uses `create_device_snapshots` before the run and `finalize_and_merge` after run.
|
|
84
|
+
"""
|
|
85
|
+
@functools.wraps(func)
|
|
86
|
+
def wrapper(self: "KeaTestRunner", *args, **kwargs):
|
|
87
|
+
# Pre-run snapshot creation
|
|
88
|
+
if self.options.download_fbm:
|
|
89
|
+
create_device_snapshots(self.options)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
return func(self, *args, **kwargs)
|
|
93
|
+
finally:
|
|
94
|
+
# Post-run finalize/merge
|
|
95
|
+
if self.options.upload_fbm:
|
|
96
|
+
finalize_and_merge(self.options)
|
|
97
|
+
|
|
98
|
+
return wrapper
|
|
@@ -18,7 +18,6 @@ from unittest import main as unittest_main
|
|
|
18
18
|
from dataclasses import dataclass, asdict, fields, is_dataclass
|
|
19
19
|
from datetime import datetime
|
|
20
20
|
from fnmatch import fnmatchcase
|
|
21
|
-
|
|
22
21
|
import uiautomator2 as u2
|
|
23
22
|
|
|
24
23
|
|
|
@@ -33,6 +32,7 @@ from .fastbotManager import FastbotManager
|
|
|
33
32
|
from .adbUtils import ADBDevice
|
|
34
33
|
from .state import invariant, INVARIANT_MARKER
|
|
35
34
|
from .result import KeaJsonResult, KeaTextTestResult
|
|
35
|
+
from .fbm_plugin import merge_fbm
|
|
36
36
|
|
|
37
37
|
logger = getLogger(__name__)
|
|
38
38
|
hybrid_mode = ContextVar("hybrid_mode", default=False)
|
|
@@ -400,18 +400,11 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter, SetUpClassExtension):
|
|
|
400
400
|
logger.info(f"Result file: {stamp_manager.result_file}")
|
|
401
401
|
logger.info(f"Property execution info file: {stamp_manager.prop_exec_file}")
|
|
402
402
|
|
|
403
|
+
@merge_fbm
|
|
403
404
|
def run(self, test):
|
|
404
405
|
|
|
405
|
-
|
|
406
|
-
# take device-side snapshots once at the beginning of the run
|
|
407
|
-
try:
|
|
408
|
-
self._copy_fbm()
|
|
409
|
-
except Exception as e:
|
|
410
|
-
logger.debug(f"Initial device snapshot failed: {e}")
|
|
411
|
-
|
|
412
406
|
self.validateAndCollectProperties(test)
|
|
413
407
|
|
|
414
|
-
|
|
415
408
|
if len(self.allProperties) == 0:
|
|
416
409
|
logger.warning("No property has been found.")
|
|
417
410
|
|
|
@@ -557,18 +550,12 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter, SetUpClassExtension):
|
|
|
557
550
|
print(f"Finish sending monkey events.", flush=True)
|
|
558
551
|
log_watcher.close()
|
|
559
552
|
|
|
560
|
-
# After run: compute per-device deltas and merge into PC core
|
|
561
|
-
try:
|
|
562
|
-
self._finalize_and_merge_deltas()
|
|
563
|
-
except Exception as e:
|
|
564
|
-
logger.debug(f"Finalize delta merge failed: {e}")
|
|
565
553
|
|
|
566
554
|
result.logSummary()
|
|
567
555
|
|
|
568
556
|
if self.options.agent == "u2":
|
|
569
557
|
self._generate_bug_report()
|
|
570
558
|
|
|
571
|
-
# self._upload_fbm()
|
|
572
559
|
|
|
573
560
|
self.tearDown()
|
|
574
561
|
return result
|
|
@@ -813,104 +800,6 @@ class KeaTestRunner(TextTestRunner, KeaOptionSetter, SetUpClassExtension):
|
|
|
813
800
|
# Ignore exceptions in __del__ to avoid "Exception ignored" warnings
|
|
814
801
|
pass
|
|
815
802
|
|
|
816
|
-
|
|
817
|
-
def _finalize_and_merge_deltas(self):
|
|
818
|
-
"""Pull device fbms, compute deltas (snapshot->current) and merge deltas into PC core fbm.
|
|
819
|
-
|
|
820
|
-
This function iterates over configured packages and uses pull_and_merge_to_pc which
|
|
821
|
-
already implements snapshot-aware delta merging when a snapshot is present on device.
|
|
822
|
-
"""
|
|
823
|
-
try:
|
|
824
|
-
from kea2.fbm_parser import FBMMerger
|
|
825
|
-
except Exception as e:
|
|
826
|
-
logger.debug(f"FBM merger unavailable for finalize: {e}")
|
|
827
|
-
return
|
|
828
|
-
|
|
829
|
-
merger = FBMMerger()
|
|
830
|
-
pkgs = getattr(self.options, 'packageNames', []) or []
|
|
831
|
-
for pkg in pkgs:
|
|
832
|
-
try:
|
|
833
|
-
logger.info(f"Finalizing FBM delta for package: {pkg}")
|
|
834
|
-
ok = merger.pull_and_merge_to_pc(pkg, device=self.options.serial,
|
|
835
|
-
transport_id=self.options.transport_id)
|
|
836
|
-
if ok:
|
|
837
|
-
logger.info(f"Delta merge completed for package: {pkg}")
|
|
838
|
-
else:
|
|
839
|
-
logger.debug(f"Delta merge reported failure for package: {pkg}")
|
|
840
|
-
except Exception as e:
|
|
841
|
-
logger.debug(f"Error finalizing delta for {pkg}: {e}")
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
def _copy_fbm(self):
|
|
845
|
-
"""If options.download_fbm is True, create an on-device snapshot for each package by copying
|
|
846
|
-
`/sdcard/fastbot_{pkg}.fbm` -> `/sdcard/fastbot_{pkg}.snapshot.fbm` using `adb shell cp`.
|
|
847
|
-
|
|
848
|
-
Behavior:
|
|
849
|
-
- Only runs if options.download_fbm is True.
|
|
850
|
-
- Tries `adb shell cp` up to `max_retries` times with backoff. Does NOT perform pull/push.
|
|
851
|
-
- Logs per-package success/failure and does not raise to avoid blocking startup.
|
|
852
|
-
"""
|
|
853
|
-
# if not getattr(self.options, 'download_fbm', False):
|
|
854
|
-
# return
|
|
855
|
-
|
|
856
|
-
try:
|
|
857
|
-
from kea2.adbUtils import adb_shell
|
|
858
|
-
except Exception:
|
|
859
|
-
try:
|
|
860
|
-
from adbUtils import adb_shell # type: ignore
|
|
861
|
-
except Exception as e:
|
|
862
|
-
print(f"ADB utilities not available for creating device snapshot: {e}", flush=True)
|
|
863
|
-
return
|
|
864
|
-
|
|
865
|
-
import time
|
|
866
|
-
import random
|
|
867
|
-
|
|
868
|
-
pkgs = getattr(self.options, 'packageNames', []) or []
|
|
869
|
-
for pkg in pkgs:
|
|
870
|
-
src = f"/sdcard/fastbot_{pkg}.fbm"
|
|
871
|
-
dst = f"/sdcard/fastbot_{pkg}.snapshot.fbm"
|
|
872
|
-
|
|
873
|
-
# First check if the source FBM exists on device. If not, skip this package.
|
|
874
|
-
try:
|
|
875
|
-
# use a single-string shell command so adb runs: adb -s <dev> shell "test -f <src> && echo OK || echo NO"
|
|
876
|
-
check_src = adb_shell([f'test -f "{src}" && echo OK || echo NO'], device=self.options.serial, transport_id=self.options.transport_id)
|
|
877
|
-
if not (isinstance(check_src, str) and "OK" in check_src):
|
|
878
|
-
print(f"Source FBM not found on device for package {pkg}: {src}. Skipping snapshot creation.", flush=True)
|
|
879
|
-
continue
|
|
880
|
-
except Exception as e:
|
|
881
|
-
print(f"Failed to verify source FBM existence for {pkg}: {e}. Skipping.", flush=True)
|
|
882
|
-
continue
|
|
883
|
-
|
|
884
|
-
max_retries = 3
|
|
885
|
-
success = False
|
|
886
|
-
for attempt in range(1, max_retries + 1):
|
|
887
|
-
try:
|
|
888
|
-
print(f"Attempt {attempt}: creating device snapshot: cp {src} {dst}", flush=True)
|
|
889
|
-
adb_shell(["cp", src, dst], device=self.options.serial, transport_id=self.options.transport_id)
|
|
890
|
-
|
|
891
|
-
# verify snapshot exists on device using a single-command form
|
|
892
|
-
try:
|
|
893
|
-
# verify snapshot exists on device using a single-string command (matches: adb shell "test -f ... && echo OK || echo NO")
|
|
894
|
-
verify = adb_shell([f'test -f "{dst}" && echo OK || echo NO'], device=self.options.serial, transport_id=self.options.transport_id)
|
|
895
|
-
if isinstance(verify, str) and "OK" in verify:
|
|
896
|
-
print(f"Snapshot created on device for package {pkg}: {dst}", flush=True)
|
|
897
|
-
success = True
|
|
898
|
-
break
|
|
899
|
-
else:
|
|
900
|
-
print(f"Snapshot verify failed on attempt {attempt} for {pkg}: {verify}", flush=True)
|
|
901
|
-
except Exception as ve:
|
|
902
|
-
print(f"Verification command failed after cp attempt {attempt} for {pkg}: {ve}", flush=True)
|
|
903
|
-
except Exception as e:
|
|
904
|
-
print(f"adb shell cp attempt {attempt} failed for {pkg}: {e}", flush=True)
|
|
905
|
-
|
|
906
|
-
# backoff before next attempt
|
|
907
|
-
sleep_time = min(5.0, 0.5 * (2 ** (attempt - 1))) + random.uniform(0, 0.1)
|
|
908
|
-
time.sleep(sleep_time)
|
|
909
|
-
|
|
910
|
-
if not success:
|
|
911
|
-
print(f"Giving up creating snapshot on device for {pkg} after {max_retries} attempts", flush=True)
|
|
912
|
-
|
|
913
|
-
|
|
914
803
|
class HybridTestRunner(TextTestRunner, KeaOptionSetter):
|
|
915
804
|
|
|
916
805
|
allTestCases: Dict[str, Tuple[TestCase, bool]]
|