Kea2-python 1.1.3b1__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.
Files changed (68) hide show
  1. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/Kea2_python.egg-info/PKG-INFO +2 -1
  2. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/Kea2_python.egg-info/SOURCES.txt +1 -0
  3. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/Kea2_python.egg-info/requires.txt +1 -0
  4. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/PKG-INFO +2 -1
  5. kea2_python-1.1.3b2/kea2/adbUtils.py +247 -0
  6. kea2_python-1.1.3b2/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  7. kea2_python-1.1.3b2/kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  8. kea2_python-1.1.3b2/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  9. kea2_python-1.1.3b2/kea2/assets/monkeyq.jar +0 -0
  10. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fbm_parser.py +19 -24
  11. kea2_python-1.1.3b2/kea2/fbm_plugin.py +98 -0
  12. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/keaUtils.py +2 -113
  13. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/kea_launcher.py +12 -22
  14. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/logWatcher.py +21 -6
  15. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/bug_report_generator.py +7 -0
  16. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/report_merger.py +11 -1
  17. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/templates/bug_report_template.html +160 -70
  18. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/templates/merged_bug_report_template.html +154 -69
  19. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/widget_coverage.py +18 -8
  20. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/u2Driver.py +0 -10
  21. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/pyproject.toml +2 -2
  22. kea2_python-1.1.3b1/kea2/adbUtils.py +0 -554
  23. kea2_python-1.1.3b1/kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
  24. kea2_python-1.1.3b1/kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
  25. kea2_python-1.1.3b1/kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
  26. kea2_python-1.1.3b1/kea2/assets/monkeyq.jar +0 -0
  27. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/Kea2_python.egg-info/dependency_links.txt +0 -0
  28. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/Kea2_python.egg-info/entry_points.txt +0 -0
  29. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/Kea2_python.egg-info/top_level.txt +0 -0
  30. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/LICENSE +0 -0
  31. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/README.md +0 -0
  32. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/__init__.py +0 -0
  33. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/config_version.json +0 -0
  34. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot-thirdpart.jar +0 -0
  35. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/abl.strings +0 -0
  36. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/awl.strings +0 -0
  37. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.config +0 -0
  38. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.fuzzing.strings +0 -0
  39. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.schema.strings +0 -0
  40. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.strings +0 -0
  41. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/max.tree.pruning +0 -0
  42. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/teardown.py +0 -0
  43. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_configs/widget.block.py +0 -0
  44. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
  45. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/framework.jar +0 -0
  46. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/kea2-thirdpart.jar +0 -0
  47. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/assets/quicktest.py +0 -0
  48. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/cli.py +0 -0
  49. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fastbotManager.py +0 -0
  50. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fastbotx/ActivityTimes.py +0 -0
  51. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fastbotx/ReuseEntry.py +0 -0
  52. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fastbotx/ReuseModel.py +0 -0
  53. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fastbotx/__init__.py +0 -0
  54. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/fs_lock.py +0 -0
  55. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/kea2_api.py +0 -0
  56. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/mixin.py +0 -0
  57. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/__init__.py +0 -0
  58. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/mixin.py +0 -0
  59. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/report/utils.py +0 -0
  60. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/result.py +0 -0
  61. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/resultSyncer.py +0 -0
  62. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/state.py +0 -0
  63. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/typedefs.py +0 -0
  64. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/utils.py +0 -0
  65. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/kea2/version_manager.py +0 -0
  66. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/setup.cfg +0 -0
  67. {kea2_python-1.1.3b1 → kea2_python-1.1.3b2}/tests/test_u2Selector.py +0 -0
  68. {kea2_python-1.1.3b1 → 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.3b1
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
 
@@ -12,6 +12,7 @@ kea2/adbUtils.py
12
12
  kea2/cli.py
13
13
  kea2/fastbotManager.py
14
14
  kea2/fbm_parser.py
15
+ kea2/fbm_plugin.py
15
16
  kea2/fs_lock.py
16
17
  kea2/kea2_api.py
17
18
  kea2/keaUtils.py
@@ -2,4 +2,5 @@ rtree>=1.3.0
2
2
  jinja2>=3.0.0
3
3
  uiautomator2>=3.3.3
4
4
  adbutils>=2.9.3
5
+ flatbuffers>=25.9.23
5
6
  packaging>=25.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Kea2-python
3
- Version: 1.1.3b1
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
@@ -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
- from kea2.adbUtils import pull_file
582
- except Exception:
583
- try:
584
- from adbUtils import pull_file # type: ignore
585
- except Exception as e:
586
- print("ADB utilities not available:", e)
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
- pull_file(remote, str(pulled_tmp), device=device, transport_id=transport_id)
599
+ dev.sync.pull(remote, str(pulled_tmp))
601
600
  except Exception as e:
602
- print(f"pull_file failed for {remote}: {e}")
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
- pull_file(snapshot_remote, str(pulled_snap_tmp), device=device, transport_id=transport_id)
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
- from kea2.adbUtils import adb_shell, pull_file, push_file
680
- except Exception:
681
- try:
682
- from adbUtils import adb_shell, pull_file, push_file # type: ignore
683
- except Exception as e:
684
- print("ADB utilities not available:", e)
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
- adb_shell(["cp", src, dst], device=device, transport_id=transport_id)
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
- pull_file(src, pc_tmp, device=device, transport_id=transport_id)
697
- push_file(pc_tmp, dst, device=device, transport_id=transport_id)
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]]