Kea2-python 1.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kea2/__init__.py +8 -0
- kea2/absDriver.py +56 -0
- kea2/adbUtils.py +554 -0
- kea2/assets/config_version.json +16 -0
- kea2/assets/fastbot-thirdpart.jar +0 -0
- kea2/assets/fastbot_configs/abl.strings +2 -0
- kea2/assets/fastbot_configs/awl.strings +3 -0
- kea2/assets/fastbot_configs/max.config +7 -0
- kea2/assets/fastbot_configs/max.fuzzing.strings +699 -0
- kea2/assets/fastbot_configs/max.schema.strings +1 -0
- kea2/assets/fastbot_configs/max.strings +3 -0
- kea2/assets/fastbot_configs/max.tree.pruning +27 -0
- kea2/assets/fastbot_configs/teardown.py +18 -0
- kea2/assets/fastbot_configs/widget.block.py +38 -0
- kea2/assets/fastbot_libs/arm64-v8a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/armeabi-v7a/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86/libfastbot_native.so +0 -0
- kea2/assets/fastbot_libs/x86_64/libfastbot_native.so +0 -0
- kea2/assets/framework.jar +0 -0
- kea2/assets/kea2-thirdpart.jar +0 -0
- kea2/assets/monkeyq.jar +0 -0
- kea2/assets/quicktest.py +126 -0
- kea2/cli.py +216 -0
- kea2/fastbotManager.py +269 -0
- kea2/kea2_api.py +166 -0
- kea2/keaUtils.py +926 -0
- kea2/kea_launcher.py +299 -0
- kea2/logWatcher.py +92 -0
- kea2/mixin.py +0 -0
- kea2/report/__init__.py +0 -0
- kea2/report/bug_report_generator.py +879 -0
- kea2/report/mixin.py +496 -0
- kea2/report/report_merger.py +1066 -0
- kea2/report/templates/bug_report_template.html +4028 -0
- kea2/report/templates/merged_bug_report_template.html +3602 -0
- kea2/report/utils.py +10 -0
- kea2/result.py +257 -0
- kea2/resultSyncer.py +65 -0
- kea2/state.py +22 -0
- kea2/typedefs.py +32 -0
- kea2/u2Driver.py +612 -0
- kea2/utils.py +192 -0
- kea2/version_manager.py +102 -0
- kea2_python-1.1.0b1.dist-info/METADATA +447 -0
- kea2_python-1.1.0b1.dist-info/RECORD +49 -0
- kea2_python-1.1.0b1.dist-info/WHEEL +5 -0
- kea2_python-1.1.0b1.dist-info/entry_points.txt +2 -0
- kea2_python-1.1.0b1.dist-info/licenses/LICENSE +16 -0
- kea2_python-1.1.0b1.dist-info/top_level.txt +1 -0
kea2/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .keaUtils import KeaTestRunner, HybridTestRunner, keaTestLoader
|
|
2
|
+
from .keaUtils import Options
|
|
3
|
+
from .keaUtils import precondition, prob, max_tries, interruptable
|
|
4
|
+
from .keaUtils import kea2_breakpoint
|
|
5
|
+
from .state import state, invariant
|
|
6
|
+
from .kea2_api import Kea2Tester
|
|
7
|
+
from .u2Driver import U2Driver
|
|
8
|
+
from .state import state
|
kea2/absDriver.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AbstractScriptDriver(abc.ABC):
|
|
5
|
+
_instances = {}
|
|
6
|
+
def __new__(cls, *args, **kwargs):
|
|
7
|
+
if cls not in cls._instances:
|
|
8
|
+
cls._instances[cls] = super().__new__(cls)
|
|
9
|
+
return cls._instances[cls]
|
|
10
|
+
|
|
11
|
+
@abc.abstractmethod
|
|
12
|
+
def getInstance(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AbstractStaticChecker(abc.ABC):
|
|
17
|
+
_instances = {}
|
|
18
|
+
def __new__(cls, *args, **kwargs):
|
|
19
|
+
if cls not in cls._instances:
|
|
20
|
+
cls._instances[cls] = super().__new__(cls)
|
|
21
|
+
return cls._instances[cls]
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def getInstance(self):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abc.abstractmethod
|
|
28
|
+
def setHierarchy(hierarchy):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AbstractDriver(abc.ABC):
|
|
33
|
+
_instances = {}
|
|
34
|
+
def __new__(cls, *args, **kwargs):
|
|
35
|
+
if cls not in cls._instances:
|
|
36
|
+
cls._instances[cls] = super().__new__(cls)
|
|
37
|
+
return cls._instances[cls]
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
@abc.abstractmethod
|
|
41
|
+
def setDevice(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
def getScriptDriver(self) -> AbstractScriptDriver:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
@abc.abstractmethod
|
|
51
|
+
def getStaticChecker(self, hierarchy) -> AbstractStaticChecker:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
@abc.abstractmethod
|
|
56
|
+
def tearDown(self): ...
|
kea2/adbUtils.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import subprocess
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from typing import IO, Generator, Optional, List, Union, List, Optional, Set, Tuple
|
|
6
|
+
|
|
7
|
+
from adbutils import AdbDevice, adb
|
|
8
|
+
|
|
9
|
+
from .utils import getLogger
|
|
10
|
+
|
|
11
|
+
logger = getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ADBDevice(AdbDevice):
|
|
15
|
+
_instance = None
|
|
16
|
+
serial: Optional[str] = None
|
|
17
|
+
transport_id: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
def __new__(cls):
|
|
20
|
+
if cls._instance is None:
|
|
21
|
+
cls._instance = super().__new__(cls)
|
|
22
|
+
return cls._instance
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def setDevice(cls, serial: Optional[str] = None, transport_id: Optional[str] = None):
|
|
26
|
+
ADBDevice.serial = serial or ADBDevice.serial
|
|
27
|
+
ADBDevice.transport_id = transport_id or ADBDevice.transport_id
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> AdbDevice:
|
|
30
|
+
"""
|
|
31
|
+
Initializes the ADBDevice instance.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
device (str, optional): The device serial number. If None, it is resolved automatically when only one device is connected.
|
|
35
|
+
transport_id (str, optional): The transport ID for the device.
|
|
36
|
+
"""
|
|
37
|
+
if not ADBDevice.serial and not ADBDevice.transport_id:
|
|
38
|
+
devices = [d.serial for d in adb.list() if d.state == "device"]
|
|
39
|
+
if len(devices) > 1:
|
|
40
|
+
raise RuntimeError("Multiple devices connected. Please specify a device")
|
|
41
|
+
if len(devices) == 0:
|
|
42
|
+
raise RuntimeError("No device connected.")
|
|
43
|
+
ADBDevice.serial = devices[0]
|
|
44
|
+
super().__init__(client=adb, serial=ADBDevice.serial, transport_id=ADBDevice.transport_id)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def stream_shell(self) -> "StreamShell":
|
|
48
|
+
if "shell_v2" in self.get_features():
|
|
49
|
+
return ADBStreamShell_V2(session=self)
|
|
50
|
+
logger.warning("Using ADBStreamShell_V1. All output will be printed to stdout.")
|
|
51
|
+
return ADBStreamShell_V1(session=self)
|
|
52
|
+
|
|
53
|
+
def kill_proc(self, proc_name):
|
|
54
|
+
r = self.shell(f"ps -ef")
|
|
55
|
+
pids = [l for l in r.splitlines() if proc_name in l]
|
|
56
|
+
if pids:
|
|
57
|
+
logger.info(f"{proc_name} running, trying to kill it.")
|
|
58
|
+
pid = pids[0].split()[1]
|
|
59
|
+
self.shell(f"kill {pid}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class StreamShell:
|
|
63
|
+
def __init__(self, session: "ADBDevice"):
|
|
64
|
+
self.dev: ADBDevice = session
|
|
65
|
+
self._thread: threading.Thread = None
|
|
66
|
+
self._exit_code = 255
|
|
67
|
+
self.stdout = sys.stdout
|
|
68
|
+
self.stderr = sys.stderr
|
|
69
|
+
self._finished = False
|
|
70
|
+
|
|
71
|
+
def __call__(self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
72
|
+
stderr: IO = None, timeout: Union[float, None] = None) -> "StreamShell":
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def _write_stdout(self, data: bytes, decode=True):
|
|
76
|
+
text = data.decode('utf-8', errors='ignore') if decode else data
|
|
77
|
+
self.stdout.write(text)
|
|
78
|
+
self.stdout.flush()
|
|
79
|
+
|
|
80
|
+
def _write_stderr(self, data: bytes, decode=True):
|
|
81
|
+
text = data.decode('utf-8', errors='ignore') if decode else data
|
|
82
|
+
self.stderr.write(text)
|
|
83
|
+
self.stderr.flush()
|
|
84
|
+
|
|
85
|
+
def wait(self):
|
|
86
|
+
""" Wait for the shell command to finish and return the exit code.
|
|
87
|
+
Returns:
|
|
88
|
+
int: The exit code of the shell command.
|
|
89
|
+
"""
|
|
90
|
+
if self._thread:
|
|
91
|
+
self._thread.join()
|
|
92
|
+
return self._exit_code
|
|
93
|
+
|
|
94
|
+
def is_running(self) -> bool:
|
|
95
|
+
""" Check if the shell command is still running.
|
|
96
|
+
Returns:
|
|
97
|
+
bool: True if the command is still running, False otherwise.
|
|
98
|
+
"""
|
|
99
|
+
return not self._finished and self._thread and self._thread.is_alive()
|
|
100
|
+
|
|
101
|
+
def poll(self):
|
|
102
|
+
"""
|
|
103
|
+
Check if the shell command is still running.
|
|
104
|
+
Returns:
|
|
105
|
+
int: The exit code if the command has finished, None otherwise.
|
|
106
|
+
"""
|
|
107
|
+
if self._thread and self._thread.is_alive():
|
|
108
|
+
return None
|
|
109
|
+
return self._exit_code
|
|
110
|
+
|
|
111
|
+
def join(self):
|
|
112
|
+
if self._thread and self._thread.is_alive():
|
|
113
|
+
self._thread.join()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ADBStreamShell_V1(StreamShell):
|
|
117
|
+
|
|
118
|
+
def __call__(
|
|
119
|
+
self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
120
|
+
stderr: IO = None, timeout: Union[float, None] = None
|
|
121
|
+
) -> "StreamShell":
|
|
122
|
+
return self.shell_v1(cmdargs, stdout, stderr, timeout)
|
|
123
|
+
|
|
124
|
+
def shell_v1(
|
|
125
|
+
self, cmdargs: Union[List[str], str],
|
|
126
|
+
stdout: IO = None, stderr: IO = None,
|
|
127
|
+
timeout: Union[float, None] = None
|
|
128
|
+
):
|
|
129
|
+
self._finished = False
|
|
130
|
+
self.stdout: IO = stdout if stdout else sys.stdout
|
|
131
|
+
self.stderr: IO = stdout if stderr else sys.stdout
|
|
132
|
+
|
|
133
|
+
cmd = " ".join(cmdargs) if isinstance(cmdargs, list) else cmdargs
|
|
134
|
+
self._generator = self._shell_v1(cmd, timeout)
|
|
135
|
+
self._thread = threading.Thread(target=self._process_output, daemon=True)
|
|
136
|
+
self._thread.start()
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _shell_v1(self, cmdargs: str, timeout: Optional[float] = None) -> Generator[Tuple[str, str], None, None]:
|
|
141
|
+
if not isinstance(cmdargs, str):
|
|
142
|
+
raise RuntimeError("_shell_v1 args must be str")
|
|
143
|
+
MAGIC = "X4EXIT:"
|
|
144
|
+
newcmd = cmdargs + f"; echo {MAGIC}$?"
|
|
145
|
+
with self.dev.open_transport(timeout=timeout) as c:
|
|
146
|
+
c.send_command(f"shell:{newcmd}")
|
|
147
|
+
c.check_okay()
|
|
148
|
+
with c.conn.makefile("r", encoding="utf-8") as f:
|
|
149
|
+
for line in f:
|
|
150
|
+
rindex = line.rfind(MAGIC)
|
|
151
|
+
if rindex == -1:
|
|
152
|
+
yield "output", line
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
yield "exit", line[rindex + len(MAGIC):]
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
def _process_output(self):
|
|
159
|
+
try:
|
|
160
|
+
for msg_type, data in self._generator:
|
|
161
|
+
|
|
162
|
+
if msg_type == 'output':
|
|
163
|
+
self._write_stdout(data, decode=False)
|
|
164
|
+
elif msg_type == 'exit':
|
|
165
|
+
# TODO : handle exit code properly
|
|
166
|
+
# self._exit_code = int(data.strip())
|
|
167
|
+
self._exit_code = 0
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"ADBStreamShell execution error: {e}")
|
|
172
|
+
self._exit_code = -1
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ADBStreamShell_V2(StreamShell):
|
|
176
|
+
def __init__(self, session: "ADBDevice"):
|
|
177
|
+
self.dev: ADBDevice = session
|
|
178
|
+
self._thread = None
|
|
179
|
+
self._exit_code = 255
|
|
180
|
+
|
|
181
|
+
def __call__(
|
|
182
|
+
self, cmdargs: Union[List[str], str], stdout: IO = None,
|
|
183
|
+
stderr: IO = None, timeout: Union[float, None] = None
|
|
184
|
+
) -> "StreamShell":
|
|
185
|
+
return self.shell_v2(cmdargs, stdout, stderr, timeout)
|
|
186
|
+
|
|
187
|
+
def shell_v2(
|
|
188
|
+
self, cmdargs: Union[List[str], str],
|
|
189
|
+
stdout: IO = None, stderr: IO = None,
|
|
190
|
+
timeout: Union[float, None] = None
|
|
191
|
+
):
|
|
192
|
+
""" Start a shell command on the device and stream its output.
|
|
193
|
+
Args:
|
|
194
|
+
cmdargs (Union[List[str], str]): The command to execute, either as a list of arguments or a single string.
|
|
195
|
+
stdout (IO, optional): The output stream for standard output. Defaults to sys.stdout.
|
|
196
|
+
stderr (IO, optional): The output stream for standard error. Defaults to sys.stderr.
|
|
197
|
+
timeout (Union[float, None], optional): Timeout for the command execution. Defaults to None.
|
|
198
|
+
Returns:
|
|
199
|
+
ADBStreamShell: An instance of ADBStreamShell that can be used to interact with the shell command.
|
|
200
|
+
"""
|
|
201
|
+
self._finished = False
|
|
202
|
+
self.stdout: IO = stdout if stdout else sys.stdout
|
|
203
|
+
self.stderr: IO = stderr if stderr else sys.stderr
|
|
204
|
+
|
|
205
|
+
cmd = " ".join(cmdargs) if isinstance(cmdargs, list) else cmdargs
|
|
206
|
+
self._generator = self._shell_v2(cmd, timeout)
|
|
207
|
+
self._thread = threading.Thread(target=self._process_output, daemon=True)
|
|
208
|
+
self._thread.start()
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def _process_output(self):
|
|
212
|
+
try:
|
|
213
|
+
for msg_type, data in self._generator:
|
|
214
|
+
|
|
215
|
+
if msg_type == 'stdout':
|
|
216
|
+
self._write_stdout(data)
|
|
217
|
+
elif msg_type == 'stderr':
|
|
218
|
+
self._write_stderr(data)
|
|
219
|
+
elif msg_type == 'exit':
|
|
220
|
+
self._exit_code = data
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"ADBStreamShell execution error: {e}")
|
|
225
|
+
self._exit_code = -1
|
|
226
|
+
|
|
227
|
+
def _shell_v2(self, cmd, timeout) -> Generator[Tuple[str, bytes], None, None]:
|
|
228
|
+
with self.dev.open_transport(timeout=timeout) as c:
|
|
229
|
+
c.send_command(f"shell,v2:{cmd}")
|
|
230
|
+
c.check_okay()
|
|
231
|
+
|
|
232
|
+
while True:
|
|
233
|
+
header = c.read_exact(5)
|
|
234
|
+
msg_id = header[0]
|
|
235
|
+
length = int.from_bytes(header[1:5], byteorder="little")
|
|
236
|
+
|
|
237
|
+
if length == 0:
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
data = c.read_exact(length)
|
|
241
|
+
|
|
242
|
+
if msg_id == 1:
|
|
243
|
+
yield ('stdout', data)
|
|
244
|
+
elif msg_id == 2:
|
|
245
|
+
yield ('stderr', data)
|
|
246
|
+
elif msg_id == 3:
|
|
247
|
+
yield ('exit', data[0])
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def run_adb_command(cmd: List[str], timeout=10):
|
|
252
|
+
"""
|
|
253
|
+
Runs an adb command and returns its output.
|
|
254
|
+
|
|
255
|
+
Parameters:
|
|
256
|
+
cmd (list): List of adb command arguments, e.g., ["devices"].
|
|
257
|
+
timeout (int): Timeout in seconds.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
str: The standard output from the command. If an error occurs, returns None.
|
|
261
|
+
"""
|
|
262
|
+
full_cmd = ["adb"] + cmd
|
|
263
|
+
logger.debug(f"{' '.join(full_cmd)}")
|
|
264
|
+
try:
|
|
265
|
+
result = subprocess.run(full_cmd, capture_output=True, text=True, timeout=timeout)
|
|
266
|
+
if result.returncode != 0:
|
|
267
|
+
print(f"Command failed: {' '.join(full_cmd)}\nError: {result.stderr}", flush=True)
|
|
268
|
+
return "\n".join([
|
|
269
|
+
result.stdout.strip(),
|
|
270
|
+
result.stderr.strip()
|
|
271
|
+
])
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
print(f"Command timed out: {' '.join(full_cmd)}", flush=True)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
def get_devices():
|
|
277
|
+
"""
|
|
278
|
+
Retrieves the list of connected Android devices.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
list: A list of device serial numbers.
|
|
282
|
+
"""
|
|
283
|
+
output = run_adb_command(["devices", "-l"])
|
|
284
|
+
devices = []
|
|
285
|
+
if output:
|
|
286
|
+
lines = output.splitlines()
|
|
287
|
+
# The first line is usually "List of devices attached". The following lines list individual devices.
|
|
288
|
+
for line in lines[1:]:
|
|
289
|
+
if line.strip():
|
|
290
|
+
parts = line.split()
|
|
291
|
+
if len(parts) >= 2 and parts[1] == "device":
|
|
292
|
+
devices.append(parts[0])
|
|
293
|
+
return devices
|
|
294
|
+
|
|
295
|
+
def ensure_device(func):
|
|
296
|
+
"""
|
|
297
|
+
A decorator that resolves the device parameter automatically if it's not provided.
|
|
298
|
+
|
|
299
|
+
If 'device' is None or not present in the keyword arguments and only one device is connected,
|
|
300
|
+
that device will be automatically used. If no devices are connected or multiple devices are
|
|
301
|
+
connected, it raises a RuntimeError.
|
|
302
|
+
"""
|
|
303
|
+
def wrapper(*args, **kwargs):
|
|
304
|
+
devices = get_devices()
|
|
305
|
+
if kwargs.get("device") is None and kwargs.get("transport_id") is None:
|
|
306
|
+
if not devices:
|
|
307
|
+
raise RuntimeError("No connected devices.")
|
|
308
|
+
if len(devices) > 1:
|
|
309
|
+
raise RuntimeError("Multiple connected devices detected. Please specify a device.")
|
|
310
|
+
kwargs["device"] = devices[0]
|
|
311
|
+
if kwargs.get("device"):
|
|
312
|
+
output = run_adb_command(["-s", kwargs["device"], "get-state"])
|
|
313
|
+
elif kwargs.get("transport_id"):
|
|
314
|
+
output = run_adb_command(["-t", kwargs["transport_id"], "get-state"])
|
|
315
|
+
if output.strip() != "device":
|
|
316
|
+
raise RuntimeError(f"[ERROR] {kwargs['device']} not connected. Please check.\n{output}")
|
|
317
|
+
return func(*args, **kwargs)
|
|
318
|
+
return wrapper
|
|
319
|
+
|
|
320
|
+
@ensure_device
|
|
321
|
+
def adb_shell(cmd: List[str], device:Optional[str]=None, transport_id:Optional[str]=None):
|
|
322
|
+
"""
|
|
323
|
+
run adb shell commands
|
|
324
|
+
|
|
325
|
+
Parameters:
|
|
326
|
+
cmd (List[str])
|
|
327
|
+
device (str, optional): The device serial number. If None, it's resolved automatically when only one device is connected.
|
|
328
|
+
"""
|
|
329
|
+
if device:
|
|
330
|
+
return run_adb_command(["-s", device, "shell"] + cmd)
|
|
331
|
+
if transport_id:
|
|
332
|
+
return run_adb_command(["-t", transport_id, "shell"] + cmd)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@ensure_device
|
|
337
|
+
def install_app(apk_path: str, device: Optional[str]=None, transport_id:Optional[str]=None):
|
|
338
|
+
"""
|
|
339
|
+
Installs an APK application on the specified device.
|
|
340
|
+
|
|
341
|
+
Parameters:
|
|
342
|
+
apk_path (str): The local path to the APK file.
|
|
343
|
+
device (str, optional): The device serial number. If None, it's resolved automatically when only one device is connected.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
str: The output from the install command.
|
|
347
|
+
"""
|
|
348
|
+
if device:
|
|
349
|
+
return run_adb_command(["-s", device, "install", apk_path])
|
|
350
|
+
if transport_id:
|
|
351
|
+
return run_adb_command(["-t", transport_id, "install", apk_path])
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@ensure_device
|
|
355
|
+
def uninstall_app(package_name: str, device: Optional[str] = None, transport_id:Optional[str]=None):
|
|
356
|
+
"""
|
|
357
|
+
Uninstalls an app from the specified device.
|
|
358
|
+
|
|
359
|
+
Parameters:
|
|
360
|
+
package_name (str): The package name of the app.
|
|
361
|
+
device (str, optional): The device serial number. If None, it's resolved automatically when only one device is connected.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
str: The output from the uninstall command.
|
|
365
|
+
"""
|
|
366
|
+
if device:
|
|
367
|
+
return run_adb_command(["-s", device, "uninstall", package_name])
|
|
368
|
+
if transport_id:
|
|
369
|
+
return run_adb_command(["-t", transport_id, "uninstall", package_name])
|
|
370
|
+
|
|
371
|
+
@ensure_device
|
|
372
|
+
def push_file(local_path: str, remote_path: str, device: Optional[str] = None, transport_id:Optional[str]=None):
|
|
373
|
+
"""
|
|
374
|
+
Pushes a file to the specified device.
|
|
375
|
+
|
|
376
|
+
Parameters:
|
|
377
|
+
local_path (str): The local file path.
|
|
378
|
+
remote_path (str): The destination path on the device.
|
|
379
|
+
device (str, optional): The device serial number. If None, it's resolved automatically when only one device is connected.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
str: The output from the push command.
|
|
383
|
+
"""
|
|
384
|
+
local_path = str(local_path)
|
|
385
|
+
remote_path = str(remote_path)
|
|
386
|
+
if device:
|
|
387
|
+
return run_adb_command(["-s", device, "push", local_path, remote_path])
|
|
388
|
+
if transport_id:
|
|
389
|
+
return run_adb_command(["-t", transport_id, "push", local_path, remote_path])
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@ensure_device
|
|
393
|
+
def pull_file(remote_path: str, local_path: str, device: Optional[str] = None, transport_id:Optional[str]=None):
|
|
394
|
+
"""
|
|
395
|
+
Pulls a file from the device to a local path.
|
|
396
|
+
|
|
397
|
+
Parameters:
|
|
398
|
+
remote_path (str): The file path on the device.
|
|
399
|
+
local_path (str): The local destination path.
|
|
400
|
+
device (str, optional): The device serial number. If None, it's resolved automatically when only one device is connected.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
str: The output from the pull command.
|
|
404
|
+
"""
|
|
405
|
+
if device:
|
|
406
|
+
return run_adb_command(["-s", device, "pull", remote_path, local_path])
|
|
407
|
+
if transport_id:
|
|
408
|
+
return run_adb_command(["-t", transport_id, "pull", remote_path, local_path])
|
|
409
|
+
|
|
410
|
+
# Forward-related functions
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@ensure_device
|
|
414
|
+
def list_forwards(device: Optional[str] = None):
|
|
415
|
+
"""
|
|
416
|
+
Lists current port forwarding rules on the specified device.
|
|
417
|
+
|
|
418
|
+
Parameters:
|
|
419
|
+
device (str, optional): The device serial number. If None, it is resolved automatically.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
list: A list of forwarding rules. Each rule is a dictionary with keys: device, local, remote.
|
|
423
|
+
"""
|
|
424
|
+
output = run_adb_command(["-s", device, "forward", "--list"])
|
|
425
|
+
forwards = []
|
|
426
|
+
if output:
|
|
427
|
+
lines = output.splitlines()
|
|
428
|
+
for line in lines:
|
|
429
|
+
parts = line.split()
|
|
430
|
+
if len(parts) == 3:
|
|
431
|
+
# Each line is expected to be: <device> <local> <remote>
|
|
432
|
+
rule = {"device": parts[0], "local": parts[1], "remote": parts[2]}
|
|
433
|
+
if rule["device"] == device:
|
|
434
|
+
forwards.append(rule)
|
|
435
|
+
else:
|
|
436
|
+
forwards.append(line)
|
|
437
|
+
return forwards
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@ensure_device
|
|
441
|
+
def create_forward(local_spec: str, remote_spec: str, device: Optional[str] = None):
|
|
442
|
+
"""
|
|
443
|
+
Creates a port forwarding rule on the specified device.
|
|
444
|
+
|
|
445
|
+
Parameters:
|
|
446
|
+
local_spec (str): The local forward specification (e.g., "tcp:8000").
|
|
447
|
+
remote_spec (str): The remote target specification (e.g., "tcp:9000").
|
|
448
|
+
device (str, optional): The device serial number. If None, it is resolved automatically.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
str: The output from the forward creation command.
|
|
452
|
+
"""
|
|
453
|
+
return run_adb_command(["-s", device, "forward", local_spec, remote_spec])
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@ensure_device
|
|
457
|
+
def remove_forward(local_spec, device: Optional[str] = None):
|
|
458
|
+
"""
|
|
459
|
+
Removes a specific port forwarding rule on the specified device.
|
|
460
|
+
|
|
461
|
+
Parameters:
|
|
462
|
+
local_spec (str): The local forward specification to remove (e.g., "tcp:8000").
|
|
463
|
+
device (str, optional): The device serial number. If None, it is resolved automatically.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
str: The output from the forward removal command.
|
|
467
|
+
"""
|
|
468
|
+
return run_adb_command(["-s", device, "forward", "--remove", local_spec])
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
@ensure_device
|
|
472
|
+
def remove_all_forwards(device: Optional[str] = None):
|
|
473
|
+
"""
|
|
474
|
+
Removes all port forwarding rules on the specified device.
|
|
475
|
+
|
|
476
|
+
Parameters:
|
|
477
|
+
device (str, optional): The device serial number. If None, it is resolved automatically.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
str: The output from the command to remove all forwards.
|
|
481
|
+
"""
|
|
482
|
+
return run_adb_command(["-s", device, "forward", "--remove-all"])
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@ensure_device
|
|
486
|
+
def get_packages(device: Optional[str]=None, transport_id: Optional[str]=None) -> Set[str]:
|
|
487
|
+
"""
|
|
488
|
+
Retrieves packages that match the specified regular expression pattern.
|
|
489
|
+
|
|
490
|
+
Parameters:
|
|
491
|
+
pattern (str): Regular expression pattern to match package names.
|
|
492
|
+
device (str, optional): The device serial number. If None, it is resolved automatically.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
set: A set of package names that match the pattern.
|
|
496
|
+
"""
|
|
497
|
+
import re
|
|
498
|
+
|
|
499
|
+
if device:
|
|
500
|
+
cmd = ["-s", device, "shell", "pm", "list", "packages"]
|
|
501
|
+
if transport_id:
|
|
502
|
+
cmd = ["-t", transport_id, "shell", "pm", "list", "packages"]
|
|
503
|
+
output = run_adb_command(cmd)
|
|
504
|
+
|
|
505
|
+
packages = set()
|
|
506
|
+
if output:
|
|
507
|
+
compiled_pattern = re.compile(r"package:(.+)\n")
|
|
508
|
+
matches = compiled_pattern.findall(output)
|
|
509
|
+
for match in matches:
|
|
510
|
+
if match:
|
|
511
|
+
packages.add(match)
|
|
512
|
+
|
|
513
|
+
return packages
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
if __name__ == '__main__':
|
|
517
|
+
# For testing: print the list of currently connected devices.
|
|
518
|
+
adb_shell(["ls", "vendor"], transport_id="2")
|
|
519
|
+
devices = get_devices()
|
|
520
|
+
if devices:
|
|
521
|
+
print("Connected devices:", flush=True)
|
|
522
|
+
for dev in devices:
|
|
523
|
+
print(f" - {dev}", flush=True)
|
|
524
|
+
else:
|
|
525
|
+
print("No devices connected.", flush=True)
|
|
526
|
+
|
|
527
|
+
# Example usage of forward-related functionalities:
|
|
528
|
+
try:
|
|
529
|
+
# List current forwards
|
|
530
|
+
forwards = list_forwards()
|
|
531
|
+
print("Current forward rules:", flush=True)
|
|
532
|
+
for rule in forwards:
|
|
533
|
+
print(rule, flush=True)
|
|
534
|
+
|
|
535
|
+
# Create a forward rule (example: forward local tcp 8000 to remote tcp 9000)
|
|
536
|
+
output = create_forward("tcp:8000", "tcp:9000")
|
|
537
|
+
print("Create forward output:", output, flush=True)
|
|
538
|
+
|
|
539
|
+
# List forwards again
|
|
540
|
+
forwards = list_forwards()
|
|
541
|
+
print("Forward rules after creation:", flush=True)
|
|
542
|
+
for rule in forwards:
|
|
543
|
+
print(rule, flush=True)
|
|
544
|
+
|
|
545
|
+
# Remove the forward rule
|
|
546
|
+
output = remove_forward("tcp:8000")
|
|
547
|
+
print("Remove forward output:", output, flush=True)
|
|
548
|
+
|
|
549
|
+
# Remove all forwards (if needed)
|
|
550
|
+
# output = remove_all_forwards()
|
|
551
|
+
# print("Remove all forwards output:", output)
|
|
552
|
+
|
|
553
|
+
except RuntimeError as e:
|
|
554
|
+
print("Error:", e, flush=True)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compatibility infos": [
|
|
3
|
+
{
|
|
4
|
+
"name": "previous version",
|
|
5
|
+
"description": "The default initial version, <=0.3.6",
|
|
6
|
+
"from": "0.0.0",
|
|
7
|
+
"to": "0.3.6"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "Hybrid test version 1.0.0",
|
|
11
|
+
"description": "Hybrid test was added in version 1.0.0. hybrid_test_config.py is required.",
|
|
12
|
+
"from": "1.0.0",
|
|
13
|
+
"to": ""
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
Binary file
|