From 0328f2e89aec316cbd27bf9683d05eb0eae9a6c5 Mon Sep 17 00:00:00 2001 From: codeskyblue Date: Mon, 25 Mar 2024 15:39:48 +0800 Subject: [PATCH] add codecov, fix coveragerc (#106) * add codecov, fix coveragerc * add more tests, remove useless reqs, close #104 * remove too many RuntimeError, use AdbError instead --- .coveragerc | 36 +++++---- README.md | 5 +- adbutils/__init__.py | 4 +- adbutils/_adb.py | 19 +++-- adbutils/_device.py | 6 +- adbutils/_proto.py | 2 +- adbutils/_utils.py | 10 ++- adbutils/errors.py | 4 + adbutils/pidcat.py | 1 - adbutils/server/__init__.py | 58 -------------- codecov.yml | 2 + requirements.txt | 1 - tests/adb_server.py | 152 ++++++++++++++++++++++++++++++++++++ tests/conftest.py | 27 ++++--- tests/test_adb.py | 8 -- tests/test_adb_server.py | 20 +++++ 16 files changed, 242 insertions(+), 113 deletions(-) delete mode 100644 adbutils/server/__init__.py create mode 100644 codecov.yml create mode 100644 tests/adb_server.py delete mode 100644 tests/test_adb.py create mode 100644 tests/test_adb_server.py diff --git a/.coveragerc b/.coveragerc index 7fa3882..4c0a967 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,25 +1,29 @@ [run] branch = true +omit = + tests/* + test_real_device/* + docs/* + setup.py + build_wheel.py + [report] -# Regexes for lines to exclude from consideration +; Regexes for lines to exclude from consideration exclude_also = - # Don't complain about missing debug-only code: - "def __repr__", - "if self\\.debug", + ; Don't complain about missing debug-only code: + def __repr__ + if self\.debug - # Don't complain if tests don't hit defensive assertion code: - "raise AssertionError", - "raise NotImplementedError", + ; Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError - # Don't complain if non-runnable code isn't run: - "if 0:", - "if __name__ == .__main__.:", + ; Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: - # Don't complain about abstract methods, they aren't run: - "@(abc\\.)?abstractmethod", + ; Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod -ignore_errors = true -omit = - "tests/*", - "docs/*" \ No newline at end of file +ignore_errors = True diff --git a/README.md b/README.md index 31fe8a0..bbdd628 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # adbutils [![PyPI](https://img.shields.io/pypi/v/adbutils.svg?color=blue)](https://pypi.org/project/adbutils/#history) +[![codecov](https://codecov.io/gh/openatx/adbutils/graph/badge.svg?token=OuGOMZUkmi)](https://codecov.io/gh/openatx/adbutils) -Python adb library for adb service (Only support Python3.6+) +Python adb library for adb service (Only support Python3.6+), Recommend 3.8+ **Table of Contents** @@ -522,7 +523,7 @@ gh-md-toc --insert README.md # Thanks - [swind pure-python-adb](https://github.com/Swind/pure-python-adb) - [openstf/adbkit](https://github.com/openstf/adbkit) -- [ADB Source Code](https://github.com/aosp-mirror/platform_system_core/blob/master/adb) +- [ADB Source Code](https://android.googlesource.com/platform/system/core/+/android-4.4_r1/adb/adb.c) - ADB Protocols [OVERVIEW.TXT](https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/OVERVIEW.TXT) [SERVICES.TXT](https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SERVICES.TXT) [SYNC.TXT](https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SYNC.TXT) - [Awesome ADB](https://github.com/mzlogin/awesome-adb) - [JakeWharton/pidcat](https://github.com/JakeWharton/pidcat) diff --git a/adbutils/__init__.py b/adbutils/__init__.py index 5ac69e6..4be86f7 100644 --- a/adbutils/__init__.py +++ b/adbutils/__init__.py @@ -76,9 +76,9 @@ def device(self, if not serial: ds = self.device_list() if len(ds) == 0: - raise RuntimeError("Can't find any android device/emulator") + raise AdbError("Can't find any android device/emulator") if len(ds) > 1: - raise RuntimeError( + raise AdbError( "more than one device/emulator, please specify the serial number" ) return ds[0] diff --git a/adbutils/_adb.py b/adbutils/_adb.py index 753f475..cc30346 100644 --- a/adbutils/_adb.py +++ b/adbutils/_adb.py @@ -16,10 +16,9 @@ from deprecation import deprecated from adbutils._utils import adb_path -from adbutils.errors import AdbError, AdbTimeout +from adbutils.errors import AdbConnectionError, AdbError, AdbTimeout from adbutils._proto import * -from adbutils._utils import list2cmdline from adbutils._version import __version__ _OKAY = "OKAY" @@ -30,15 +29,15 @@ def _check_server(host: str, port: int) -> bool: """ Returns if server is running """ s = socket.socket() try: + s.settimeout(.1) s.connect((host, port)) return True - except socket.error as e: + except (socket.timeout, socket.error) as e: return False finally: s.close() - class AdbConnection(object): def __init__(self, host: str, port: int): self.__host = host @@ -52,16 +51,20 @@ def _create_socket(self): adb_port = self.__port s = socket.socket() try: + s.settimeout(.1) # prevent socket hang s.connect((adb_host, adb_port)) + s.settimeout(None) return s - except: - s.close() - raise + except socket.timeout as e: + raise AdbTimeout("connect to adb server timeout") + except socket.error as e: + raise AdbConnectionError("connect to adb server failed: %s" % e) + def _safe_connect(self): try: return self._create_socket() - except ConnectionRefusedError: + except AdbConnectionError: subprocess.run([adb_path(), "start-server"], timeout=20.0) # 20s should enough for adb start return self._create_socket() diff --git a/adbutils/_device.py b/adbutils/_device.py index ae337b2..b070cd3 100644 --- a/adbutils/_device.py +++ b/adbutils/_device.py @@ -97,7 +97,7 @@ def open_transport(self, elif self._serial: c.send_command(f"host-serial:{self._serial}:{command}") else: - raise RuntimeError + raise RuntimeError("should not reach here") c.check_okay() else: if self._transport_id: @@ -108,7 +108,7 @@ def open_transport(self, # so here use host:transport c.send_command(f"host:transport:{self._serial}") else: - raise RuntimeError + raise RuntimeError("should not reach here") c.check_okay() return c @@ -575,7 +575,7 @@ def iter_content(self, path: str) -> typing.Iterator[bytes]: chunk_size = struct.unpack(" bytes: - body = "{:04x}".format(n) - header = "{:04x}".format(len(body)) - return (header + body).encode() - - -async def adb_server(): - host = '127.0.0.1' - port = 7305 - - server = await asyncio.start_server(handle_command, host, port) - - # Print server info - addr = server.sockets[0].getsockname() - print(f'ADB server listening on {addr}') - - async with server: - # Keep running the server - await server.serve_forever() - - -def run_adb_server(): - asyncio.run(adb_server()) - - -if __name__ == '__main__': - run_adb_server() \ No newline at end of file diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..2974137 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +codecov: + token: 59cb8341-87db-45bd-b828-e8ae19cd4062 diff --git a/requirements.txt b/requirements.txt index b04e1d6..002b0ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -whichcraft requests deprecation>=2.0.6,<3.0 retry>=0.9 diff --git a/tests/adb_server.py b/tests/adb_server.py new file mode 100644 index 0000000..e8cc346 --- /dev/null +++ b/tests/adb_server.py @@ -0,0 +1,152 @@ +# +# Created on Sun Mar 24 2024 codeskyblue +# + +from __future__ import annotations + +import asyncio +import functools +import logging +from typing import overload + +logger = logging.getLogger(__name__) + + + +@overload +def encode(data: str) -> bytes: + ... + +@overload +def encode(data: bytes) -> bytes: + ... + +@overload +def encode(data: int) -> bytes: + ... + + +def encode(data): + if isinstance(data, bytes): + return encode_bytes(data) + if isinstance(data, int): + return encode_number(data) + if isinstance(data, str): + return encode_string(data) + raise ValueError("data must be bytes or int") + + +def encode_number(n: int) -> bytes: + body = "{:04x}".format(n) + return encode_bytes(body.encode()) + +def encode_string(s: str, encoding: str = 'utf-8') -> bytes: + return encode_bytes(s.encode(encoding)) + +def encode_bytes(s: bytes) -> bytes: + header = "{:04x}".format(len(s)).encode() + return header + s + + + +COMMANDS: dict[str, callable] = {} + +def register_command(name: str): + def wrapper(func): + COMMANDS[name] = func + return func + return wrapper + + +class Context: + def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, server: "AdbServer" = None): + self.reader = reader + self.writer = writer + self.server = server + + async def send(self, data: bytes): + self.writer.write(data) + await self.writer.drain() + + async def recv(self, length: int) -> bytes: + return await self.reader.read(length) + + async def close(self): + self.writer.close() + await self.writer.wait_closed() + + +@register_command("host:version") +async def host_version(ctx: Context): + await ctx.send(b"OKAY") + await ctx.send(encode_number(1234)) + + +@register_command("host:kill") +async def host_kill(ctx: Context): + await ctx.send(b"OKAY") + await ctx.close() + await ctx.server.stop() + # os.kill(os.getpid(), signal.SIGINT) + + +async def handle_command(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, server: "AdbServer"): + try: + # Receive the command from the client + addr = writer.get_extra_info('peername') + logger.info(f"Connection from %s", addr) + cmd_length = int((await reader.readexactly(4)).decode(), 16) + command = (await reader.read(cmd_length)).decode() + logger.info("recv command: %s", command) + if command not in COMMANDS: + writer.write(b"FAIL") + writer.write(encode(f"Unknown command: {command}")) + await writer.drain() + writer.close() + return + ctx = Context(reader, writer, server) + await COMMANDS[command](ctx) + await ctx.close() + except asyncio.IncompleteReadError: + pass + + + +class AdbServer: + def __init__(self, port: int = 7305, host: str = '127.0.0.1'): + self.port = port + self.host = host + self.server = None + + async def start(self): + _handle = functools.partial(handle_command, server=self) + self.server = await asyncio.start_server(_handle, self.host, self.port) + addr = self.server.sockets[0].getsockname() + print(f'ADB server listening on {addr}') + + async with self.server: + try: + # Keep running the server + await self.server.serve_forever() + except asyncio.CancelledError: + pass + + async def stop(self): + self.server.close() + await self.server.wait_closed() + + + +async def adb_server(): + host = '127.0.0.1' + port = 7305 + await AdbServer(port, host).start() + + +def run_adb_server(): + logging.basicConfig(level=logging.DEBUG) + asyncio.run(adb_server()) + + +if __name__ == '__main__': + run_adb_server() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 8a93669..234e0f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,32 +4,41 @@ import logging import threading import adbutils -from adbutils.server import run_adb_server import pytest import time import socket +from adb_server import run_adb_server -def wait_for_port(port, timeout=10): +def check_port(port) -> bool: + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.settimeout(0.1) + s.connect(('127.0.0.1', port)) + return True + except (ConnectionRefusedError, OSError, socket.timeout): + return False + + +def wait_for_port(port, timeout:float=3, ready: bool = True): start_time = time.time() while True: if time.time() - start_time > timeout: raise TimeoutError(f"Port {port} is not being listened to within {timeout} seconds") - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(0.1) - s.connect(('localhost', port)) + if check_port(port) == ready: return - except (ConnectionRefusedError, OSError, socket.timeout): - time.sleep(0.1) + time.sleep(0.1) + -@pytest.fixture(scope='session') +@pytest.fixture(scope='function') def adb_server_fixture(): th = threading.Thread(target=run_adb_server, name='mock-adb-server') th.daemon = True th.start() wait_for_port(7305) + yield + adbutils.AdbClient(port=7305).server_kill() diff --git a/tests/test_adb.py b/tests/test_adb.py deleted file mode 100644 index 3bfbe35..0000000 --- a/tests/test_adb.py +++ /dev/null @@ -1,8 +0,0 @@ -# coding: utf-8 -# - - -import adbutils - -def test_server_version(adb: adbutils.AdbClient): - assert adb.server_version() == 1234 \ No newline at end of file diff --git a/tests/test_adb_server.py b/tests/test_adb_server.py new file mode 100644 index 0000000..77d6f46 --- /dev/null +++ b/tests/test_adb_server.py @@ -0,0 +1,20 @@ +# coding: utf-8 +# + + +import adbutils +from adb_server import encode + + +def test_encode(): + assert encode(1234) == b'000404d2' + + +def test_server_version(adb: adbutils.AdbClient): + assert adb.server_version() == 1234 + + +def test_server_kill(adb: adbutils.AdbClient): + adb.server_kill() + +