diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index ca7907b..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[bumpversion] -commit = True -tag = True -current_version = 0.3.0-dev -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? -serialize = - {major}.{minor}.{patch}-{release} - {major}.{minor}.{patch} - -[bumpversion:file:setup.py] - -[bumpversion:file:ppadb/__init__.py] - -[bumpversion:part:release] -optional_value = release -values = - dev - release diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b7dd304..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -adb/_version.py export-subst diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f7d021e..0000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -.idea -__pycache__ -*.png -.cache -dist -*.egg-info -.repo -.pytest_cache -*.pyc -venv -test_result.xml -.python-version \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a53568f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: python -python: -- '3.6' -install: -- pip install -r requirements.txt -script: -- echo "skip test" -deploy: - skip_cleanup: true - provider: pypi - user: swind - password: - secure: GjGXSJk5e52is++wzUv0/SmWtEAIXv7WcFq+p+SzFx4aOn+gnT6yp0zY/9C4qNFJk8p2TbCawbFicK7WUCqd0QoUTiG495SwlIEf5FHa2L0qWFVU2KLNLXB/FD2GosqnpsL/FIOwIqFzZ/PjeEpnDx45d7yZhXR571bdgMxzah89gQZgsHS2eBrjwKMNjTRpNP+hlGbiMRLyYh2Ay0UJG4wGL92FoOooboB6z473ZdyIKq5Hsah4+WhvQWV/oPwqLS18us4tF0zd4YO4cG8T+cgdIBF/+xgpQTSQEaWWAzi/f1PK99KtNJuy2NV+TFL03CMRBSfr0UDMoU4neSKVNXC51F9uNQCTQN2NGOtK2SJlPvQqbsnaN2IL83DA3r4RxHtHoY0U7AsTt448FDwrLrfMEDtOrocJ2DdxI/oBpkTB3/2eMZYzcUfRruxNsq05N4V8uhRyH4/tQnMU8+LEWdGfsgpZkb3ag3sZucdojFBwacBOgS/aOms/t3cQRT4wHLAYd4nYMz5ZzGKZOxv18+C95YEXl4L3vLB1ur1C/xPpfAMYJssN7MAZah+sWi6IOZGWa4DiITEJKf8NaIcM5WyUImxT+/TGvPSdLoUb5WgOqnBaiiK+4P8n8RzvUYfMvLp6G9mV0p7yctgUwUSr7dsuwyObn7UlNuuxAYGpvsM= - on: - tags: true diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index b7599da..0000000 --- a/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -FROM swind/docker-python3-adb:latest diff --git a/HISTORY.rst b/HISTORY.rst deleted file mode 100644 index 927f9a3..0000000 --- a/HISTORY.rst +++ /dev/null @@ -1,21 +0,0 @@ -0.2.1 (2019-10-14) --------------------- - -* Fixes #21: Rename the package name from "adb" to "ppadb" -* Fixes #23: Support push dir to device -* Fixes #25: Don't call logging.basicConfig() in the module - - -0.1.6 (2019-01-21) -------------------- - -* Fix #4 push does not preserve original timestap unlike equiv adb push from command line -* Fix #6 forward_list should also check serial -* Fix #8: adb/command/host/__init__.py can take an exception parsing "devices" data - - -0.1.0 (2018-06-23) -------------------- - -* First release on PyPI. - diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2f586c4..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 CloudMosa - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9ef07db..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -recursive-exclude test * -include README.rst -include HISTORY.rst diff --git a/README.rst b/README.rst deleted file mode 100644 index b333257..0000000 --- a/README.rst +++ /dev/null @@ -1,329 +0,0 @@ -The package name has been renamed from 'adb' to 'ppadb' -========================================================= - -From version **v0.2.1-dev**, the package name has been renamed from 'adb' to 'ppadb' to avoid conflit with Google `google/python-adb`_ - - -Introduction -================== - -This is pure-python implementation of the ADB client. - -You can use it to communicate with adb server (not the adb daemon on the device/emulator). - -When you use `adb` command - -.. image:: https://raw.githubusercontent.com/Swind/pure-python-adb/master/docs/adb_cli.png - -Now you can use `pure-python-adb` to connect to adb server as adb command line - -.. image:: https://raw.githubusercontent.com/Swind/pure-python-adb/master/docs/adb_pure_python_adb.png - -This package supports most of the adb command line tool's functionality. - -1. adb devices -2. adb shell -3. adb forward -4. adb pull/push -5. adb install/uninstall - -Requirements -============ - -Python 3.6+ - -Installation -============ - -.. code-block:: console - - $pip install -U pure-python-adb - -Examples -======== - -Connect to adb server and get the version ------------------------------------------ - -.. code-block:: python - - from ppadb.client import Client as AdbClient - # Default is "127.0.0.1" and 5037 - client = AdbClient(host="127.0.0.1", port=5037) - print(client.version()) - - >>> 39 - -Connect to a device -------------------- - -.. code-block:: python - - from ppadb.client import Client as AdbClient - # Default is "127.0.0.1" and 5037 - client = AdbClient(host="127.0.0.1", port=5037) - device = client.device("emulator-5554") - - -List all devices ( adb devices ) and install/uninstall an APK on all devices ----------------------------------------------------------------------------------- - -.. code-block:: python - - from ppadb.client import Client as AdbClient - - apk_path = "example.apk" - - # Default is "127.0.0.1" and 5037 - client = AdbClient(host="127.0.0.1", port=5037) - devices = client.devices() - - for device in devices: - device.install(apk_path) - - # Check apk is installed - for device in devices: - print(device.is_installed("example.package")) - - # Uninstall - for device in devices: - device.uninstall("example.package") - -adb shell ---------- - -.. code-block:: python - - from ppadb.client import Client as AdbClient - # Default is "127.0.0.1" and 5037 - client = AdbClient(host="127.0.0.1", port=5037) - device = client.device("emulator-5554") - device.shell("echo hello world !") - -.. code-block:: python - - def dump_logcat(connection): - while True: - data = connection.read(1024) - if not data: - break - print(data.decode('utf-8')) - - connection.close() - - from ppadb.client import Client as AdbClient - # Default is "127.0.0.1" and 5037 - client = AdbClient(host="127.0.0.1", port=5037) - device = client.device("emulator-5554") - device.shell("logcat", handler=dump_logcat) - -read logcat line by line - -.. code-block:: python - - from ppadb.client import Client - - def dump_logcat_by_line(connect): - file_obj = connect.socket.makefile() - for index in range(0, 10): - print("Line {}: {}".format(index, file_obj.readline().strip())) - - file_obj.close() - connect.close() - - client = Client() - device = client.device("emulator-5554") - device.shell("logcat", handler=dump_logcat_by_line) - -Screenshot ----------- - -.. code-block:: python - - from ppadb.client import Client as AdbClient - client = AdbClient(host="127.0.0.1", port=5037) - device = client.device("emulator-5554") - result = device.screencap() - with open("screen.png", "wb") as fp: - fp.write(result) - -Push file or folder --------------------- - -.. code-block:: python - - from ppadb.client import Client as AdbClient - client = AdbClient(host="127.0.0.1", port=5037) - device = client.device("emulator-5554") - - device.push("example.apk", "/sdcard/example.apk") - -Pull ----- - -.. code-block:: python - - from ppadb.client import Client as AdbClient - client = AdbClient(host="127.0.0.1", port=5037) - device = client.device("emulator-5554") - - device.shell("screencap -p /sdcard/screen.png") - device.pull("/sdcard/screen.png", "screen.png") - -Connect to device ------------------ - -.. code-block:: python - - from ppadb.client import Client as AdbClient - client = AdbClient(host="127.0.0.1", port=5037) - client.remote_connect("172.20.0.1", 5555) - - device = client.device("172.20.0.1:5555") - - # Disconnect all devices - client.remote_disconnect() - - ##Disconnect 172.20.0.1 - # client.remote_disconnect("172.20.0.1") - ##Or - # client.remote_disconnect("172.20.0.1", 5555) - - -Enable debug logger --------------------- - -.. code-block:: python - - logging.getLogger("ppadb").setLevel(logging.DEBUG) - -Async Client --------------------- - -.. code-block:: python - - import asyncio - import aiofiles - from ppadb.client_async import ClientAsync as AdbClient - - async def _save_screenshot(device): - result = await device.screencap() - file_name = f"{device.serial}.png" - async with aiofiles.open(f"{file_name}", mode='wb') as f: - await f.write(result) - - return file_name - - async def main(): - client = AdbClient(host="127.0.0.1", port=5037) - devices = await client.devices() - for device in devices: - print(device.serial) - - result = await asyncio.gather(*[_save_screenshot(device) for device in devices]) - print(result) - - asyncio.run(main()) - - - - - - -How to run test cases -====================== - -Prepare --------- - -1. Install Docker - -2. Install Docker Compose - -.. code-block:: console - - pip install docker-compose - -3. Modify `test/conftest.py` - -Change the value of `adb_host` to the "emulator" - -.. code-block:: python - - adb_host="emulator" - -4. Run testcases - -.. code-block:: console - - docker-compose up - -Result - -.. code-block:: console - - Starting purepythonadb_emulator_1 ... done - Recreating purepythonadb_python_environment_1 ... done - Attaching to purepythonadb_emulator_1, purepythonadb_python_environment_1 - emulator_1 | + echo n - emulator_1 | + /home/user/android-sdk-linux/tools/bin/avdmanager create avd -k system-images;android-25;google_apis;x86 -n Docker -b x86 -g google_apis --device 8 --force - Parsing /home/user/android-sdk-linux/emulator/package.xmlParsing /home/user/android-sdk-linux/patcher/v4/package.xmlParsing /home/user/android-sdk-linux/platform-tools/package.xmlParsing /home/user/android-sdk-linux/platforms/android-25/package.xmlParsing /home/user/android-sdk-linux/system-images/android-25/google_apis/x86/package.xmlParsing /home/user/android-sdk-linux/tools/package.xml+ echo hw.keyboard = true - emulator_1 | + adb start-server - emulator_1 | * daemon not running; starting now at tcp:5037 - python_environment_1 | ============================= test session starts ============================== - python_environment_1 | platform linux -- Python 3.6.1, pytest-3.6.3, py-1.5.4, pluggy-0.6.0 - python_environment_1 | rootdir: /code, inifile: - python_environment_1 | collected 27 items - python_environment_1 | - emulator_1 | * daemon started successfully - emulator_1 | + exec /usr/bin/supervisord - emulator_1 | /usr/lib/python2.7/dist-packages/supervisor/options.py:298: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security. - emulator_1 | 'Supervisord is running as root and it is searching ' - emulator_1 | 2018-07-07 17:19:47,560 CRIT Supervisor running as root (no user in config file) - emulator_1 | 2018-07-07 17:19:47,560 INFO Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing - emulator_1 | 2018-07-07 17:19:47,570 INFO RPC interface 'supervisor' initialized - emulator_1 | 2018-07-07 17:19:47,570 CRIT Server 'unix_http_server' running without any HTTP authentication checking - emulator_1 | 2018-07-07 17:19:47,570 INFO supervisord started with pid 1 - emulator_1 | 2018-07-07 17:19:48,573 INFO spawned: 'socat-5554' with pid 74 - emulator_1 | 2018-07-07 17:19:48,574 INFO spawned: 'socat-5555' with pid 75 - emulator_1 | 2018-07-07 17:19:48,576 INFO spawned: 'socat-5037' with pid 76 - emulator_1 | 2018-07-07 17:19:48,578 INFO spawned: 'novnc' with pid 77 - emulator_1 | 2018-07-07 17:19:48,579 INFO spawned: 'socat-9008' with pid 78 - emulator_1 | 2018-07-07 17:19:48,582 INFO spawned: 'emulator' with pid 80 - emulator_1 | 2018-07-07 17:19:49,607 INFO success: socat-5554 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) - emulator_1 | 2018-07-07 17:19:49,607 INFO success: socat-5555 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) - emulator_1 | 2018-07-07 17:19:49,607 INFO success: socat-5037 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) - emulator_1 | 2018-07-07 17:19:49,607 INFO success: novnc entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) - emulator_1 | 2018-07-07 17:19:49,608 INFO success: socat-9008 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) - emulator_1 | 2018-07-07 17:19:49,608 INFO success: emulator entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) - python_environment_1 | test/test_device.py .............. [ 51%] - python_environment_1 | test/test_host.py .. [ 59%] - python_environment_1 | test/test_host_serial.py ........ [ 88%] - python_environment_1 | test/test_plugins.py ... [100%] - python_environment_1 | - python_environment_1 | ------------------ generated xml file: /code/test_result.xml ------------------- - python_environment_1 | ========================= 27 passed in 119.15 seconds ========================== - purepythonadb_python_environment_1 exited with code 0 - Aborting on container exit... - Stopping purepythonadb_emulator_1 ... done - -More Information -================= - -A pure Node.js client for the Android Debug Bridge ---------------------------------------------------- - -adbkit_ - -ADB documents --------------- - -- protocol_ -- services_ -- sync_ - -.. _adbkit: https://github.com/openstf/stf -.. _protocol: https://android.googlesource.com/platform/system/core/+/master/adb/protocol.txt -.. _services: https://android.googlesource.com/platform/system/core/+/master/adb/SERVICES.TXT -.. _sync: https://android.googlesource.com/platform/system/core/+/master/adb/SYNC.TXT -.. _`google/python-adb`: https://github.com/google/python-adb diff --git a/ppadb/__init__.py b/__init__.py similarity index 100% rename from ppadb/__init__.py rename to __init__.py diff --git a/__pycache__/__init__.cpython-38.pyc b/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..fc00198 Binary files /dev/null and b/__pycache__/__init__.cpython-38.pyc differ diff --git a/__pycache__/client_async.cpython-38.pyc b/__pycache__/client_async.cpython-38.pyc new file mode 100644 index 0000000..9b2f376 Binary files /dev/null and b/__pycache__/client_async.cpython-38.pyc differ diff --git a/__pycache__/connection_async.cpython-38.pyc b/__pycache__/connection_async.cpython-38.pyc new file mode 100644 index 0000000..d0846cb Binary files /dev/null and b/__pycache__/connection_async.cpython-38.pyc differ diff --git a/__pycache__/device_async.cpython-38.pyc b/__pycache__/device_async.cpython-38.pyc new file mode 100644 index 0000000..20a9e88 Binary files /dev/null and b/__pycache__/device_async.cpython-38.pyc differ diff --git a/__pycache__/protocol.cpython-38.pyc b/__pycache__/protocol.cpython-38.pyc new file mode 100644 index 0000000..95ccbc5 Binary files /dev/null and b/__pycache__/protocol.cpython-38.pyc differ diff --git a/ppadb/application.py b/application.py similarity index 100% rename from ppadb/application.py rename to application.py diff --git a/bumpversion.sh b/bumpversion.sh deleted file mode 100755 index 36eb61c..0000000 --- a/bumpversion.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -bump2version --no-tag minor ppadb/__init__.py - diff --git a/ppadb/client.py b/client.py similarity index 100% rename from ppadb/client.py rename to client.py diff --git a/ppadb/client_async.py b/client_async.py similarity index 100% rename from ppadb/client_async.py rename to client_async.py diff --git a/ppadb/command/__init__.py b/command/__init__.py similarity index 100% rename from ppadb/command/__init__.py rename to command/__init__.py diff --git a/command/__pycache__/__init__.cpython-38.pyc b/command/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..97fd7ff Binary files /dev/null and b/command/__pycache__/__init__.cpython-38.pyc differ diff --git a/ppadb/command/host/__init__.py b/command/host/__init__.py similarity index 100% rename from ppadb/command/host/__init__.py rename to command/host/__init__.py diff --git a/ppadb/command/host_async/__init__.py b/command/host_async/__init__.py similarity index 100% rename from ppadb/command/host_async/__init__.py rename to command/host_async/__init__.py diff --git a/command/host_async/__pycache__/__init__.cpython-38.pyc b/command/host_async/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..803122d Binary files /dev/null and b/command/host_async/__pycache__/__init__.cpython-38.pyc differ diff --git a/ppadb/command/serial/__init__.py b/command/serial/__init__.py similarity index 100% rename from ppadb/command/serial/__init__.py rename to command/serial/__init__.py diff --git a/ppadb/command/transport/__init__.py b/command/transport/__init__.py similarity index 100% rename from ppadb/command/transport/__init__.py rename to command/transport/__init__.py diff --git a/ppadb/command/transport_async/__init__.py b/command/transport_async/__init__.py similarity index 100% rename from ppadb/command/transport_async/__init__.py rename to command/transport_async/__init__.py diff --git a/command/transport_async/__pycache__/__init__.cpython-38.pyc b/command/transport_async/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..e23d264 Binary files /dev/null and b/command/transport_async/__pycache__/__init__.cpython-38.pyc differ diff --git a/ppadb/connection.py b/connection.py similarity index 100% rename from ppadb/connection.py rename to connection.py diff --git a/ppadb/connection_async.py b/connection_async.py similarity index 100% rename from ppadb/connection_async.py rename to connection_async.py diff --git a/ppadb/device.py b/device.py similarity index 100% rename from ppadb/device.py rename to device.py diff --git a/device_async.py b/device_async.py new file mode 100644 index 0000000..b2b7719 --- /dev/null +++ b/device_async.py @@ -0,0 +1,133 @@ +try: + from asyncio import get_running_loop +except ImportError: # pragma: no cover + from asyncio import get_event_loop as get_running_loop # Python 3.6 compatibility + +import re +import os + +from ppadb import InstallError +from ppadb.command.transport_async import TransportAsync +from ppadb.sync_async import SyncAsync + +from ppadb.utils.logger import AdbLogging + +try: + from shlex import quote as cmd_quote +except ImportError: + from pipes import quote as cmd_quote + + +logger = AdbLogging.get_logger(__name__) + + +def _get_src_info(src): + exists = os.path.exists(src) + isfile = os.path.isfile(src) + isdir = os.path.isdir(src) + basename = os.path.basename(src) + walk = None if not isdir else list(os.walk(src)) + + return exists, isfile, isdir, basename, walk + + +class DeviceAsync(TransportAsync): + INSTALL_RESULT_PATTERN = "(Success|Failure|Error)\s?(.*)" + UNINSTALL_RESULT_PATTERN = "(Success|Failure.*|.*Unknown package:.*)" + + def __init__(self, client, serial): + self.client = client + self.serial = serial + + async def create_connection(self, set_transport=True, timeout=None): + conn = await self.client.create_connection(timeout=timeout) + + if set_transport: + await self.transport(conn) + + return conn + + async def _push(self, src, dest, mode, progress): + # Create a new connection for file transfer + sync_conn = await self.sync() + sync = SyncAsync(sync_conn) + + async with sync_conn: + await sync.push(src, dest, mode, progress) + + + async def install(self, path, + forward_lock=False, # -l + reinstall=False, # -r + test=True, # -t + installer_package_name="", # -i {installer_package_name} + shared_mass_storage=False, # -s + internal_system_memory=False, # -f + downgrade=False, # -d + grand_all_permissions=True # -g + ): + dest = SyncAsync.temp(path) + + parameters = [] + if forward_lock: parameters.append("-l") + if reinstall: parameters.append("-r") + if test: parameters.append("-t") + if len(installer_package_name) > 0: parameters.append("-i {}".format(installer_package_name)) + if shared_mass_storage: parameters.append("-s") + if internal_system_memory: parameters.append("-f") + if downgrade: parameters.append("-d") + if grand_all_permissions: parameters.append("-g") + + try: + result = await self.shell( + "pm install {} {}".format(" ".join(parameters), cmd_quote(dest)) + ) + match = re.search(self.INSTALL_RESULT_PATTERN, result) + + if match and match.group(1) == "Success": + return True + elif match: + groups = match.groups() + raise InstallError(dest, groups[1]) + else: + raise InstallError(dest, result) + finally: + await self.shell("rm -f {}".format(dest)) + + async def uninstall(self, package="com.github.uiautomator2"): + result = await self.shell("pm uninstall {}".format(package)) + + m = re.search(self.UNINSTALL_RESULT_PATTERN, result) + + if m and m.group(1) == "Success": + return True + elif m: + logger.error(m.group(1)) + return False + else: + logger.error("There is no message after uninstalling") + return False + + async def push(self, src, dest, mode=0o644, progress=None): + exists, isfile, isdir, basename, walk = await get_running_loop().run_in_executor(None, _get_src_info, src) + if not exists: + raise FileNotFoundError("Cannot find {}".format(src)) + + if isfile: + await self._push(src, dest, mode, progress) + + elif isdir: + for root, dirs, files in walk: + root_dir_path = os.path.join(basename, root.replace(src, "")) + + await self.shell("mkdir -p {}/{}".format(dest, root_dir_path)) + + for item in files: + await self._push(os.path.join(root, item), os.path.join(dest, root_dir_path, item), mode, progress) + + async def pull(self, src, dest): + sync_conn = await self.sync() + sync = SyncAsync(sync_conn) + + async with sync_conn: + return await sync.pull(src, dest) \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index db3a5ff..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,23 +0,0 @@ -version: '3' -services: - python_environment: - image: python:3 - depends_on: - - emulator - volumes: - - .:/code - working_dir: /code - environment: - - PYTHONPATH=/code - - PYTHONUNBUFFERED=0 - command: sh -c "pip install -r requirements.txt;py.test test -s -v --junit-xml test_result.xml;pip install 'aiofiles>=0.4.0';py.test test_async -s -v --junit-xml test_result_async.xml" - - emulator: - image: swind/android-emulator:android_28 - environment: - - ANDROID_AVD_EXTRA_ARGS=--device 8 --force - - ANDROID_EMULATOR_EXTRA_ARGS=-skin 1080x1920 -memory 2048 -no-boot-anim -gpu host -qemu - ports: - - 6080:6080 - devices: - - "/dev/kvm:/dev/kvm" diff --git a/docs/adb_cli.png b/docs/adb_cli.png deleted file mode 100644 index ad175a9..0000000 Binary files a/docs/adb_cli.png and /dev/null differ diff --git a/docs/adb_pure_python_adb.png b/docs/adb_pure_python_adb.png deleted file mode 100644 index cb3bfea..0000000 Binary files a/docs/adb_pure_python_adb.png and /dev/null differ diff --git a/example/screencap.py b/example/screencap.py deleted file mode 100644 index e8f62a8..0000000 --- a/example/screencap.py +++ /dev/null @@ -1,8 +0,0 @@ -from ppadb.client import Client as AdbClient - -client = AdbClient(host="127.0.0.1", port=5037) -device = client.device("emulator-5554") - -device.push("./screencap.py", "/sdcard/screencap.py") - - diff --git a/ppadb/keycode.py b/keycode.py similarity index 100% rename from ppadb/keycode.py rename to keycode.py diff --git a/ppadb/plugins/__init__.py b/plugins/__init__.py similarity index 100% rename from ppadb/plugins/__init__.py rename to plugins/__init__.py diff --git a/ppadb/plugins/client/__init__.py b/plugins/client/__init__.py similarity index 100% rename from ppadb/plugins/client/__init__.py rename to plugins/client/__init__.py diff --git a/ppadb/plugins/device/__init__.py b/plugins/device/__init__.py similarity index 100% rename from ppadb/plugins/device/__init__.py rename to plugins/device/__init__.py diff --git a/ppadb/plugins/device/batterystats.py b/plugins/device/batterystats.py similarity index 100% rename from ppadb/plugins/device/batterystats.py rename to plugins/device/batterystats.py diff --git a/ppadb/plugins/device/batterystats_section.py b/plugins/device/batterystats_section.py similarity index 100% rename from ppadb/plugins/device/batterystats_section.py rename to plugins/device/batterystats_section.py diff --git a/ppadb/plugins/device/cpustat.py b/plugins/device/cpustat.py similarity index 100% rename from ppadb/plugins/device/cpustat.py rename to plugins/device/cpustat.py diff --git a/ppadb/plugins/device/input.py b/plugins/device/input.py similarity index 100% rename from ppadb/plugins/device/input.py rename to plugins/device/input.py diff --git a/ppadb/plugins/device/traffic.py b/plugins/device/traffic.py similarity index 100% rename from ppadb/plugins/device/traffic.py rename to plugins/device/traffic.py diff --git a/ppadb/plugins/device/utils.py b/plugins/device/utils.py similarity index 100% rename from ppadb/plugins/device/utils.py rename to plugins/device/utils.py diff --git a/ppadb/plugins/device/wm.py b/plugins/device/wm.py similarity index 100% rename from ppadb/plugins/device/wm.py rename to plugins/device/wm.py diff --git a/ppadb/device_async.py b/ppadb/device_async.py deleted file mode 100644 index 5e4a8b0..0000000 --- a/ppadb/device_async.py +++ /dev/null @@ -1,69 +0,0 @@ -try: - from asyncio import get_running_loop -except ImportError: # pragma: no cover - from asyncio import get_event_loop as get_running_loop # Python 3.6 compatibility - -import re -import os - -from ppadb.command.transport_async import TransportAsync -from ppadb.sync_async import SyncAsync - - -def _get_src_info(src): - exists = os.path.exists(src) - isfile = os.path.isfile(src) - isdir = os.path.isdir(src) - basename = os.path.basename(src) - walk = None if not isdir else list(os.walk(src)) - - return exists, isfile, isdir, basename, walk - - -class DeviceAsync(TransportAsync): - INSTALL_RESULT_PATTERN = "(Success|Failure|Error)\s?(.*)" - UNINSTALL_RESULT_PATTERN = "(Success|Failure.*|.*Unknown package:.*)" - - def __init__(self, client, serial): - self.client = client - self.serial = serial - - async def create_connection(self, set_transport=True, timeout=None): - conn = await self.client.create_connection(timeout=timeout) - - if set_transport: - await self.transport(conn) - - return conn - - async def _push(self, src, dest, mode, progress): - # Create a new connection for file transfer - sync_conn = await self.sync() - sync = SyncAsync(sync_conn) - - async with sync_conn: - await sync.push(src, dest, mode, progress) - - async def push(self, src, dest, mode=0o644, progress=None): - exists, isfile, isdir, basename, walk = await get_running_loop().run_in_executor(None, _get_src_info, src) - if not exists: - raise FileNotFoundError("Cannot find {}".format(src)) - - if isfile: - await self._push(src, dest, mode, progress) - - elif isdir: - for root, dirs, files in walk: - root_dir_path = os.path.join(basename, root.replace(src, "")) - - await self.shell("mkdir -p {}/{}".format(dest, root_dir_path)) - - for item in files: - await self._push(os.path.join(root, item), os.path.join(dest, root_dir_path, item), mode, progress) - - async def pull(self, src, dest): - sync_conn = await self.sync() - sync = SyncAsync(sync_conn) - - async with sync_conn: - return await sync.pull(src, dest) diff --git a/ppadb/utils/logger.py b/ppadb/utils/logger.py deleted file mode 100644 index 9ea1149..0000000 --- a/ppadb/utils/logger.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging - -logging.getLogger("ppadb").setLevel(logging.CRITICAL) - -class AdbLogging: - PACKAGE_NAME = "ppadb" - DEFAULT_FORMAT = logging.Formatter(fmt='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', - datefmt='%d-%m-%Y:%H:%M:%S') - DEFAULT_LEVEL = logging.CRITICAL - - @classmethod - def get_logger(cls, name): - if not name.startswith(cls.PACKAGE_NAME): - raise RuntimeError("The package logger name should be 'adb.xxx.xxx' but {}".format(name)) - - logger = logging.getLogger(name) - if logger.handlers: - logger.handlers[0].setFormatter(cls.DEFAULT_FORMAT) - - return logger - - @classmethod - def set_default_format(cls, format): - cls.DEFAULT_FORMAT = format - - @classmethod - def disable(cls): - logging.getLogger(cls.PACKAGE_NAME).setLevel(logging.CRITICAL) - - @classmethod - def enable(cls, level=logging.DEBUG): - logging.getLogger(cls.PACKAGE_NAME).setLevel(level) diff --git a/ppadb/protocol.py b/protocol.py similarity index 100% rename from ppadb/protocol.py rename to protocol.py diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 55b033e..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pytest \ No newline at end of file diff --git a/scripts/batterystats_codegen.py b/scripts/batterystats_codegen.py deleted file mode 100644 index de276af..0000000 --- a/scripts/batterystats_codegen.py +++ /dev/null @@ -1,127 +0,0 @@ -# This csv is converted from https://developer.android.com/studio/command-line/dumpsys#battery -raw_batterystats_fields = """ -vers,Version,"checkin version, parcel version, start platform version, end platform version" -uid,UID,"uid, package name" -apk,APK,"wakeups, APK, service, start time, starts, launches" -pr,Process,"process, user, system, foreground, starts" -sr,Sensor,"sensor number, time, count" -vib,Vibrator,"time, count" -fg,Foreground,"time, count" -st,State Time,"foreground, active, running" -wl,Wake lock,"wake lock, full time, 'f', full count, partial time, 'p', partial count, window time, 'w', window count" -sy,Sync,"sync, time, count" -jb,Job,"job, time, count" -kwl,Kernel Wake Lock,"kernel wake lock, time, count" -wr,Wakeup Reason,"wakeup reason, time, count" -nt,Network,"mobile bytes RX, mobile bytes TX, Wi-Fi bytes RX, Wi-Fi bytes TX, mobile packets RX, mobile packets TX, Wi-Fi packets RX, Wi-Fi packets TX, mobile active time, mobile active count" -ua,User Activity,"other, button, touch" -bt,Battery,"start count, battery realtime, battery uptime, total realtime, total uptime, start clock time, battery screen off realtime, battery screen off uptime" -dc,Battery Discharge,"low, high, screen on, screen off" -lv,Battery Level,"start level, current level" -wfl,Wi-Fi,"full Wi-Fi lock on time, Wi-Fi scan time, Wi-Fi running time, Wi-Fi scan count, Wi-Fi idle time, Wi-Fi receive time, Wi-Fi transmit time" -gwfl,Global Wi-Fi,"Wi-Fi on time, Wi-Fi running time, Wi-Fi idle time, Wi-Fi receive time, Wi-Fi transmit time, Wi-Fi power (mAh)" -gble,Global Bluetooth,"BT idle time, BT receive time, BT transmit time, BT power (mAh)" -m,Misc,"screen on time, phone on time, full wakelock time total, partial wakelock time total, mobile radio active time, mobile radio active adjusted time, interactive time, power save mode enabled time, connectivity changes, device idle mode enabled time, device idle mode enabled count, device idling time, device idling count, mobile radio active count, mobile radio active unknown time" -gn,Global Network,"mobile RX total bytes, mobile TX total bytes, Wi-Fi RX total bytes, Wi-Fi TX total bytes, mobile RX total packets, mobile TX total packets, Wi-Fi RX total packets, Wi-Fi TX total packets" -br,Screen Brightness,"dark, dim, medium, light, bright" -sst,Signal Scanning Time,signal scanning time -sgt,Signal Strength Time,"none, poor, moderate, good, great" -sgc,Signal Strength Count,"none, poor, moderate, good, great" -dct,Data Connection Time,"none, GPRS, EDGE, UMTS, CDMA, EVDO_0, EVDO_A, 1xRTT, HSDPA, HSUPA, HSPA, IDEN, EVDO_B, LTE, EHRPD, HSPAP, other" -dcc,Data Connection Count,"none, GPRS, EDGE, UMTS, CDMA, EVDO_0, EVDO_A, 1xRTT, HSDPA, HSUPA, HSPA, IDEN, EVDO_B, LTE, EHRPD, HSPAP, other" -wst,Wi-Fi State Time,"off, off scanning, on no networks, on disconnected, on connected STA, on connected P2P, on connected STA P2P, soft AP" -wsc,Wi-Fi State Count,"off, off scanning, on no networks, on disconnected, on connected STA, on connected P2P, on connected STA P2P, soft AP" -wsst,Wi-Fi Supplicant State Time,"invalid, disconnected, interface disabled, inactive, scanning, authenticating, associating, associated, four-way handshake, group handshake, completed, dormant, uninitialized" -wssc,Wi-Fi Supplicant State Count,"invalid, disconnected, interface disabled, inactive, scanning, authenticating, associating, associated, four-way handshake, group handshake, completed, dormant, uninitialized" -wsgt,Wi-Fi Signal Strength Time,"none, poor, moderate, good, great" -wsgc,Wi-Fi Signal Strength Count,"none, poor, moderate, good, great" -bst,Bluetooth State Time,"inactive, low, med, high" -bsc,Bluetooth State Count,"inactive, low, med, high" -""" - -""" -class Version: - def __init__(self, checkin_version, parcel_version, start_platform_version, end_platform_version): - self._id = "vers" - self.checkin_version = checkin_version - self.parcel_version = parcel_version - self.start_platform_version = start_platform_version - self.end_platform_version = end_platform_version -""" - -CLASS_TEMPLATE = """ -class {CLASS_NAME}: - def __init__(self, {VARS}): - self.id = "{ID}" -{INIT_VARS} -""" - -GET_SECTION = """ -def get_section(name): - return mapping.get(name) -""" - -class BatteryStatsSection: - def __init__(self, id, description, fields): - self.id = id - self.description = description - self.remaining_fields = fields - - @staticmethod - def Load(raw): - sections = [] - for line in raw.split('\n'): - if not line: - continue - id, description, raw_fields = line.split(",", 2) - - if raw_fields.strip().startswith('"'): - remaining_fields = list(map(lambda item: item.strip(), raw_fields[1:-1].split(","))) - else: - remaining_fields = [raw_fields.strip()] - - sections.append(BatteryStatsSection(id, description, remaining_fields)) - - return sections - - -def convert_to_var_name(field): - return field.replace(" ", "_")\ - .replace("-", "_")\ - .replace("'", "")\ - .lower()\ - .replace("wi_fi", "wifi")\ - .replace("1x", "_1x")\ - .replace("(mah)", "mah") - -def generate(): - sections = BatteryStatsSection.Load(raw_batterystats_fields) - code_blocks = [] - mapping = [] - for section in sections: - class_name = section.description.replace(" ", "").replace("-", "") - vars = list(map(lambda item: convert_to_var_name(item), section.remaining_fields)) - init_vars = list(map(lambda item: " self.{} = {}".format(item, item), vars)) - code = CLASS_TEMPLATE.format( - ID=section.id, - CLASS_NAME=class_name, - VARS=",".join(vars), - INIT_VARS="\n".join(init_vars) - ) - code_blocks.append(code) - mapping.append("'{}':{}".format(section.id, class_name)) - - mapping_code = "mapping={\n" + ",\n".join(mapping) + "\n}" - - return "\n".join(code_blocks) + "\n" + mapping_code + "\n" + GET_SECTION - -if __name__ == "__main__": - import os - - codegen = generate() - folder_path = os.path.dirname(__file__) - file_path = os.path.join(folder_path, "..", "ppadb", "plugins", "device", "batterystats_section.py") - with open(file_path, "w") as fp: - fp.write(codegen) - - diff --git a/setup.py b/setup.py deleted file mode 100644 index 7cf8795..0000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""The setup script.""" - -from setuptools import setup, find_packages - -with open('README.rst') as readme_file: - readme = readme_file.read() - -with open('HISTORY.rst') as history_file: - history = history_file.read() - -classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Testing', -] - -setup( - name='pure-python-adb', - version="0.3.0-dev", - description='Pure python implementation of the adb client', - long_description=readme + '\n\n' + history, - author='Swind Ou', - author_email='swind@cloudmosa.com', - url="https://github.com/Swind/pure-python-adb", - license='MIT license', - packages=find_packages(exclude=["*.test", "*.test.*", "test.*", "test"]), - install_requires=[], - extras_require={"async": ["aiofiles>=0.4.0"]}, - keywords="adb", - classifiers=classifiers, -) diff --git a/ppadb/sync/__init__.py b/sync/__init__.py similarity index 100% rename from ppadb/sync/__init__.py rename to sync/__init__.py diff --git a/sync/__pycache__/__init__.cpython-38.pyc b/sync/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..08083c7 Binary files /dev/null and b/sync/__pycache__/__init__.cpython-38.pyc differ diff --git a/sync/__pycache__/stats.cpython-38.pyc b/sync/__pycache__/stats.cpython-38.pyc new file mode 100644 index 0000000..fa401bd Binary files /dev/null and b/sync/__pycache__/stats.cpython-38.pyc differ diff --git a/ppadb/sync/stats.py b/sync/stats.py similarity index 100% rename from ppadb/sync/stats.py rename to sync/stats.py diff --git a/ppadb/sync_async/__init__.py b/sync_async/__init__.py similarity index 93% rename from ppadb/sync_async/__init__.py rename to sync_async/__init__.py index 4f65540..0ad719a 100644 --- a/ppadb/sync_async/__init__.py +++ b/sync_async/__init__.py @@ -20,7 +20,7 @@ def _get_src_info(src): if not exists: return exists, None, None - timestamp = os.stat(src).st_mtime + timestamp = int(os.stat(src).st_mtime) total_size = os.path.getsize(src) return exists, timestamp, total_size @@ -29,9 +29,15 @@ def _get_src_info(src): class SyncAsync: DATA_MAX_LENGTH = 65536 + TEMP_PATH = '/data/local/tmp' + def __init__(self, connection): self.connection = connection + @staticmethod + def temp(path): + return "{}/{}".format(SyncAsync.TEMP_PATH, os.path.basename(path)) + async def push(self, src, dest, mode, progress=None): """Push from local path |src| to |dest| on device. :param progress: callback, called with (filename, total_size, sent_size) @@ -122,4 +128,4 @@ async def _send_str(self, cmd, args): le_args_len = self._little_endian(len(args)) data = cmd.encode() + le_args_len + args logger.debug("Send string: {}".format(data)) - await self.connection.write(data) + await self.connection.write(data) \ No newline at end of file diff --git a/sync_async/__pycache__/__init__.cpython-38.pyc b/sync_async/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..f64c1a2 Binary files /dev/null and b/sync_async/__pycache__/__init__.cpython-38.pyc differ diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index a0e1ced..0000000 --- a/test/conftest.py +++ /dev/null @@ -1,123 +0,0 @@ -import time -import telnetlib - -import pytest - -from ppadb.client import Client as AdbClient -from ppadb.device import Device as AdbDevice -import logging - -logger = logging.getLogger(__name__) - -adb_host = "emulator" -#adb_host = "127.0.0.1" -#adb_host = "172.20.0.2" -adb_port = 5037 -device_serial = "emulator-5554" -emulator_port = 5554 - -class EmulatorConsole: - def __init__(self, host, port): - self._port = port - self._telnet = telnetlib.Telnet(host=host, port=port) - print(self._telnet.read_until(b"OK", timeout=5)) - - def send(self, data): - self._telnet.write(data.encode('utf-8') + b'\n') - return self._telnet.read_until(b"OK", timeout=5).decode('utf-8').strip() - - def is_live(self): - result = self.send("ping") - if "I am alive!" in result: - return True - else: - return False - - def kill(self): - self.send("kill") - return True - -def wait_until_true(check_fn, timeout=10, description=None, interval=1): - start_time = time.time() - duration = 0 - while True: - elapsed_seconds = time.time() - start_time - if elapsed_seconds >= timeout: - return False - - if check_fn(): - return True - else: - if description: - msg = description - else: - msg = "Wait until {} return True...".format(check_fn.__name__ + "()") - - elapsed_seconds = int(elapsed_seconds) - if duration != elapsed_seconds: - duration = elapsed_seconds - logger.info("{}... {}s (timeout:{})".format(msg, elapsed_seconds, timeout)) - - time.sleep(interval) - -@pytest.fixture(scope="session") -def serial(request): - return device_serial - -@pytest.fixture(scope="session") -def client(request): - logger.info("Connecting to adb server {}:{}...".format(adb_host, adb_port)) - client = AdbClient(host=adb_host, port=adb_port) - - def try_to_connect_to_adb_server(): - try: - client.version() - return True - except Exception: - return False - - wait_until_true(try_to_connect_to_adb_server, - timeout=60, - description="Try to connect to adb server {}:{}".format(adb_host, adb_port)) - - logger.info("Adb server version: {}".format(client.version())) - - return client - - -@pytest.fixture(scope="session") -def device(request, client, serial): - def emulator_console_is_connectable(): - try: - console = EmulatorConsole(host=adb_host, port=emulator_port) - return console - except Exception as e: - return None - - def is_boot_completed(): - try: - adb_device = client.device(serial) - result = adb_device.shell("getprop sys.boot_completed") - if not result: - return False - - result = int(result.strip()) - - if result == 1: - return True - else: - return False - except ValueError as e: - logger.warning(e) - return False - except Exception as e: - logger.error(e) - return False - - result = wait_until_true(emulator_console_is_connectable, timeout=60) - assert result, "Can't connect to the emulator console" - - result = wait_until_true(is_boot_completed, timeout=60) - assert result, "The emulator doesn't boot" - - return AdbDevice(client, "emulator-5554") diff --git a/test/resources/apk/app-armeabi-v7a.apk b/test/resources/apk/app-armeabi-v7a.apk deleted file mode 100644 index b8afb92..0000000 Binary files a/test/resources/apk/app-armeabi-v7a.apk and /dev/null differ diff --git a/test/resources/apk/app-x86.apk b/test/resources/apk/app-x86.apk deleted file mode 100644 index 735541e..0000000 Binary files a/test/resources/apk/app-x86.apk and /dev/null differ diff --git a/test/test_batterystats.py b/test/test_batterystats.py deleted file mode 100644 index e3a5f5a..0000000 --- a/test/test_batterystats.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest - -@pytest.mark.skip -def test_get_batterystats(device): - assert device.get_batterystats() is not None - -def test_get_battery_level(device): - result = device.get_battery_level() - assert result == 100 diff --git a/test/test_cpu_stat.py b/test/test_cpu_stat.py deleted file mode 100644 index 9fd7646..0000000 --- a/test/test_cpu_stat.py +++ /dev/null @@ -1,11 +0,0 @@ -def test_get_cpu_times(device): - result = device.cpu_times() - assert result is not None - -def test_get_cpu_percent(device): - percent = device.cpu_percent(interval=1) - assert percent is not None - assert percent != 0 - -def test_get_cpu_count(device): - assert device.cpu_count() == 2 diff --git a/test/test_device.py b/test/test_device.py deleted file mode 100644 index 9df7566..0000000 --- a/test/test_device.py +++ /dev/null @@ -1,222 +0,0 @@ -import os -import time - -import pytest -import socket - -from ppadb import ClearError, InstallError - - -def test_install_uninstall_success(device): - dir_path = os.path.dirname(os.path.realpath(__file__)) - result = device.install(os.path.join(dir_path, "resources/apk/app-x86.apk"), - reinstall=True, - downgrade=True) - assert result is True - - with pytest.raises(InstallError) as excinfo: - device.install(os.path.join(dir_path, "resources/apk/app-x86.apk")) - - assert "INSTALL_FAILED_ALREADY_EXISTS" in str(excinfo.value) - - result = device.is_installed('com.cloudmosa.helloworldapk') - assert result is True - - result = device.uninstall("com.cloudmosa.helloworldapk") - assert result is True - - result = device.uninstall("com.cloudmosa.helloworldapk") - assert result is False - - result = device.is_installed('com.cloudmosa.helloworldapk') - assert result is False - - -def test_uninstall_not_exist_package(device): - result = device.uninstall("com.cloudmosa.not.exist") - assert result is False - - -def test_list_features(device): - features = device.list_features() - assert "reqGlEsVersion" in features - assert "android.hardware.sensor.barometer" in features - - assert features["reqGlEsVersion"] == "0x20000" - assert features["android.hardware.sensor.barometer"] is True - - -def test_clear(device): - result = device.clear("com.android.chrome") - assert result is True - - with pytest.raises(ClearError) as excinfo: - result = device.clear("com.android.not.exist.package") - - assert "Package com.android.not.exist.package could not be cleared - [Failed]" in str(excinfo.value) - - -def test_list_packages(device): - packages = device.list_packages() - - assert "com.android.chrome" in packages - assert "com.android.shell" in packages - - -def test_get_properties(device): - properties = device.get_properties() - - assert "ro.product.device" in properties - assert "ro.product.model" in properties - - -def test_list_reverses(device): - result = device.list_reverses() - assert result is not None - - -def test_reboot_than_wait_boot(device): - result = device.reboot() - assert device.wait_boot_complete() is True - - -def test_get_version_name(device): - result = device.get_package_version_name("com.android.chrome") - assert result is not None - - -def test_shell_echo_sleep_long_time(device): - result = device.shell("sleep 30;echo passed") - assert "passed" in result - - -def test_shell_echo_timeout(device): - with pytest.raises(socket.timeout) as excinfo: - device.shell("sleep 60;echo passed", timeout=10) - - assert "timed out" in str(excinfo.value) - - -def test_get_top_activity(device): - activity = device.get_top_activity() - assert activity is not None - - -def test_get_top_activities(device): - activities = device.get_top_activities() - assert len(activities) != 0 - - -def test_pull(device): - import hashlib - - device.shell("screencap -p /sdcard/screen.png") - checksum = device.shell("md5sum -b /sdcard/screen.png").strip() - - device.pull("/sdcard/screen.png", "./screen.png") - hash_md5 = hashlib.md5() - with open("./screen.png", "rb") as fp: - for chunk in iter(lambda: fp.read(4096), b""): - hash_md5.update(chunk) - - pull_checksum = hash_md5.hexdigest() - - assert checksum == pull_checksum - - -def test_push_stat(device): - dir_path = os.path.dirname(os.path.realpath(__file__)) - apk_path = os.path.join(dir_path, "resources/apk/app-x86.apk") - device.push(apk_path, "/sdcard/test.apk") - - stat = os.stat(apk_path) - result = device.shell("stat /sdcard/test.apk -c %X") - if int(device.shell("getprop ro.build.version.sdk")) >= 27: - assert int(result) == int(stat.st_mtime) - else: - timestamp = int(time.time()) - assert timestamp - 10 <= int(result) <= timestamp + 10 - - -def test_push_dir(device): - dir_path = os.path.dirname(os.path.realpath(__file__)) - apk_path = os.path.join(dir_path, "resources/apk") - device.push(apk_path, "/sdcard") - - result = device.shell("ls /sdcard/apk") - assert "app-armeabi-v7a.apk" in result - assert "app-x86.apk" in result - - -def test_push_with_progress(device): - dir_path = os.path.dirname(os.path.realpath(__file__)) - apk_path = os.path.join(dir_path, "resources/apk/app-x86.apk") - - result = [] - - def progress(file_name, total_size, sent_size): - result.append({ - "file_name": file_name, - "total_size": total_size, - "sent_size": sent_size - }) - - device.push(apk_path, "/sdcard/test.apk", progress=progress) - - assert result - assert result[-1]["total_size"] == result[-1]["sent_size"] - - -def test_forward(device): - device.killforward_all() - forward_map = device.list_forward() - assert not forward_map - - device.forward("tcp:6000", "tcp:7000") - device.forward("tcp:6001", "tcp:7001") - device.forward("tcp:6002", "tcp:7002") - - forward_map = device.list_forward() - assert forward_map['tcp:6000'] == "tcp:7000" - assert forward_map['tcp:6001'] == "tcp:7001" - assert forward_map['tcp:6002'] == "tcp:7002" - - device.killforward("tcp:6000") - forward_map = device.list_forward() - assert "tcp:6000" not in forward_map - assert forward_map['tcp:6001'] == "tcp:7001" - assert forward_map['tcp:6002'] == "tcp:7002" - - device.killforward_all() - forward_map = device.list_forward() - assert not forward_map - - -@pytest.mark.skip -def test_killforward_all(client, device): - """ - This testcase need two emulators for testing, - But the android docker container can'd execute two emulators at the same time. - If you want to execute this testcase, - you need to start two emulators 'emulator-5554' and 'emualtor-5556' on your machine. - """ - device2 = client.device("emulator-5556") - - device.forward("tcp:6001", "tcp:6001") - device2.forward("tcp:6002", "tcp:6002") - - forward_map = device.list_forward() - assert forward_map['tcp:6001'] == "tcp:6001" - assert "tcp:6002" not in forward_map - - device.killforward_all() - forward_map = device.list_forward() - assert "tcp:6001" not in forward_map - - forward_map = client.list_forward() - assert "emulator-5556" in forward_map - assert forward_map["emulator-5556"]["tcp:6002"] == "tcp:6002" - - client.killforward_all() - forward_map = client.list_forward() - assert not forward_map diff --git a/test/test_host.py b/test/test_host.py deleted file mode 100644 index 20dc92e..0000000 --- a/test/test_host.py +++ /dev/null @@ -1,70 +0,0 @@ -import time - -def test_list_devices(client, serial): - devices = client.devices() - assert len(devices) > 0 - assert any(map(lambda device: device.serial == serial, devices)) - -def test_list_devices_by_state(client): - devices = client.devices(client.BOOTLOADER) - assert len(devices) == 0 - - devices = client.devices(client.OFFLINE) - assert len(devices) == 0 - - devices = client.devices(client.DEVICE) - assert len(devices) == 1 - -def test_version(client): - version = client.version() - - assert type(version) == int - assert version != 0 - -def test_list_forward(client, device, serial): - client.killforward_all() - result = client.list_forward() - assert not result - - device.forward("tcp:6000", "tcp:6000") - result = client.list_forward() - assert result[serial]["tcp:6000"] == "tcp:6000" - - client.killforward_all() - result = client.list_forward() - assert not result - - -def test_features(client): - assert client.features() - - -def test_remote_connect_disconnect(client): - host = client.host - client.remote_connect(host, 5555) - device = client.device("{}:5555".format(host)) - assert device is not None - - # Disconnect by ip - client.remote_disconnect(host) - device = client.device("{}:5555".format(host)) - assert device is None - - # Disconnect by ip and port - client.remote_connect(host, 5555) - device = client.device("{}:5555".format(host)) - assert device is not None - - # Disconnect all - client.remote_disconnect() - device = client.device("{}:5555".format(host)) - assert device is None - - for index in range(0, 10): - device = client.device("emulator-5554") - if device is not None: - break - else: - time.sleep(1) - - assert device is not None diff --git a/test/test_host_serial.py b/test/test_host_serial.py deleted file mode 100644 index 69d53ad..0000000 --- a/test/test_host_serial.py +++ /dev/null @@ -1,78 +0,0 @@ -import threading -import pytest - - -def test_shell_sync(device): - result = device.shell("dumpsys") - assert "dumpsys window sessions" in result - - -def test_shell_async(device): - result = bytearray() - - def callback(conn): - while True: - data = conn.read(1024) - if not data: - break - result.extend(bytearray(data)) - - thread = threading.Thread(target=device.shell, args=("dumpsys", callback)) - thread.start() - thread.join() - - assert "dumpsys window sessions" in result.decode('utf-8') - - -def test_get_device_path(device): - result = device.get_device_path() - assert result == 'unknown' - - -def test_get_serial_no(device, serial): - result = device.get_serial_no() - assert result == serial - - -def test_get_state(device): - result = device.get_state() - assert result == 'device' - - -def test_forward(device): - device.forward("tcp:9999", "tcp:7777") - forward_list = device.list_forward() - assert "tcp:9999" in forward_list - assert forward_list['tcp:9999'] == "tcp:7777" - - device.killforward("tcp:9999") - forward_list = device.list_forward() - assert len(forward_list) == 0 - - -def test_forward_killforward_all(device): - device.forward("tcp:9999", "tcp:7777") - forward_list = device.list_forward() - assert "tcp:9999" in forward_list - assert forward_list['tcp:9999'] == "tcp:7777" - - device.killforward_all() - forward_list = device.list_forward() - assert len(forward_list) == 0 - - -def test_forward_norebind_failed(device): - try: - device.forward("tcp:9999", "tcp:7777") - forward_list = device.list_forward() - assert "tcp:9999" in forward_list - assert forward_list['tcp:9999'] == "tcp:7777" - - with pytest.raises(RuntimeError) as excinfo: - device.forward("tcp:9999", "tcp:7777", norebind=True) - - assert "cannot rebind existing socket" in str(excinfo.value) - finally: - device.killforward_all() - forward_list = device.list_forward() - assert len(forward_list) == 0 diff --git a/test/test_logging.py b/test/test_logging.py deleted file mode 100644 index 3f70630..0000000 --- a/test/test_logging.py +++ /dev/null @@ -1,62 +0,0 @@ -from ppadb.utils.logger import AdbLogging -import logging - -def test_without_logging(capsys): - logger = AdbLogging.get_logger("ppadb.test") - logger.addHandler(logging.StreamHandler()) - - logger.info("INFO message") - captured = capsys.readouterr() - assert not captured.out - assert not captured.err - - logger.warning("WARNING message") - captured = capsys.readouterr() - assert not captured.out - assert not captured.err - - logger.debug("DEBUG message") - assert not captured.out - assert not captured.err - -def test_without_log_message_after_set_root_logger_level(capsys): - logging.basicConfig() - logger = AdbLogging.get_logger("ppadb.test") - logger.addHandler(logging.StreamHandler()) - - logging.getLogger().setLevel(logging.DEBUG) - - logger.info("INFO message") - captured = capsys.readouterr() - assert not captured.out - assert not captured.err - - logger.warning("WARNING message") - captured = capsys.readouterr() - assert not captured.out - assert not captured.err - - logger.debug("DEBUG message") - assert not captured.out - assert not captured.err - -def test_enable_log_message(capsys): - logging.basicConfig() - logger = AdbLogging.get_logger("ppadb.test") - logger.addHandler(logging.StreamHandler()) - logging.getLogger("ppadb").setLevel(logging.DEBUG) - - logger.info("INFO message") - captured = capsys.readouterr() - assert not captured.out - assert captured.err - - logger.warning("WARNING message") - captured = capsys.readouterr() - assert not captured.out - assert captured.err - - logger.debug("DEBUG message") - assert not captured.out - assert captured.err - diff --git a/test/test_plugins.py b/test/test_plugins.py deleted file mode 100644 index c55e403..0000000 --- a/test/test_plugins.py +++ /dev/null @@ -1,42 +0,0 @@ -import time - - -def open_chrome(device): - activity = "com.android.chrome/com.google.android.apps.chrome.Main" - cmd = 'am start -a android.intent.action.VIEW -n {activity} -d {url}'.format( - activity=activity, - url="https://www.google.com" - ) - device.shell(cmd) - - -def test_get_traffic(device): - open_chrome(device) - time.sleep(5) - # Get chrome traffic state - states = device.get_traffic("com.android.chrome") - - assert states is not None - assert len(states) != 0 - - -def test_get_traffic_of_not_existing_package(device): - states = device.get_traffic("com.not.existing.package") - - assert states is None - - -def test_get_cpu_stat(device): - open_chrome(device) - - pid = device.get_pid("com.android.chrome") - assert pid is not None - - total_cpu_stat = device.get_total_cpu() - process_cpu_stat = device.get_pid_cpu(pid) - - print("CPU Total: {}\n".format(total_cpu_stat)) - print("CPU Process: {}\n".format(process_cpu_stat)) - - assert total_cpu_stat is not None - assert process_cpu_stat is not None diff --git a/test/test_transport.py b/test/test_transport.py deleted file mode 100644 index e69de29..0000000 diff --git a/test_async/__init__.py b/test_async/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test_async/async_wrapper.py b/test_async/async_wrapper.py deleted file mode 100644 index eefcfda..0000000 --- a/test_async/async_wrapper.py +++ /dev/null @@ -1,20 +0,0 @@ -import asyncio -import warnings - - - -def _await(coro): - with warnings.catch_warnings(record=True) as warns: - ret = asyncio.get_event_loop().run_until_complete(coro) - - if warns: - raise RuntimeError - - return ret - - -def awaiter(func): - def sync_func(*args, **kwargs): - return _await(func(*args, **kwargs)) - - return sync_func diff --git a/test_async/patchers.py b/test_async/patchers.py deleted file mode 100644 index 8f91789..0000000 --- a/test_async/patchers.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Patches for async socket functionality.""" - -from contextlib import asynccontextmanager -from unittest.mock import patch - -try: - from unittest.mock import AsyncMock -except ImportError: - from unittest.mock import MagicMock - - class AsyncMock(MagicMock): - async def __call__(self, *args, **kwargs): - return super(AsyncMock, self).__call__(*args, **kwargs) - - -def async_mock_open(read_data=""): - class AsyncMockFile: - def __init__(self, read_data): - self.read_data = read_data - _async_mock_open.written = read_data[:0] - - async def read(self, size=-1): - if size == -1: - ret = self.read_data - self.read_data = self.read_data[:0] - return ret - - n = min(size, len(self.read_data)) - ret = self.read_data[:n] - self.read_data = self.read_data[n:] - return ret - - async def write(self, b): - if _async_mock_open.written: - _async_mock_open.written += b - else: - _async_mock_open.written = b - - @asynccontextmanager - async def _async_mock_open(*args, **kwargs): - try: - yield AsyncMockFile(read_data) - finally: - pass - - return _async_mock_open - - -class FakeStreamWriter: - def close(self): - pass - - async def wait_closed(self): - pass - - def write(self, data): - pass - - async def drain(self): - pass - - -class FakeStreamReader: - async def read(self, numbytes): - return b'TEST' - - -def async_patch(*args, **kwargs): - return patch(*args, new_callable=AsyncMock, **kwargs) diff --git a/test_async/test_client_async.py b/test_async/test_client_async.py deleted file mode 100644 index b824861..0000000 --- a/test_async/test_client_async.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Unit tests for the `ClientAsync` class. - -""" - - -import asyncio -import sys -import unittest - -sys.path.insert(0, '..') - -from ppadb.client_async import ClientAsync - -from .async_wrapper import awaiter -from .patchers import FakeStreamReader, FakeStreamWriter, async_patch - - -class TestClientAsync(unittest.TestCase): - def setUp(self): - self.client = ClientAsync() - - @awaiter - async def test_create_connection_fail(self): - with self.assertRaises(RuntimeError): - await self.client.create_connection() - - @awaiter - async def test_device_returns_none(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'0000', b'']): - self.assertIsNone(await self.client.device('serial')) - - @awaiter - async def test_device(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'000b', b'serial test']): - self.assertIsNotNone(await self.client.device('serial')) - - -if __name__ == '__main__': - unittest.main() diff --git a/test_async/test_connection_async.py b/test_async/test_connection_async.py deleted file mode 100644 index 4ff82a1..0000000 --- a/test_async/test_connection_async.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Unit tests for `ConnectionAsync` class. - -""" - - -import asyncio -import sys -import unittest -from unittest.mock import patch - -sys.path.insert(0, '..') - -from ppadb.connection_async import ConnectionAsync - -from .async_wrapper import awaiter -from .patchers import FakeStreamReader, FakeStreamWriter, async_patch - - -class TestConnectionAsync(unittest.TestCase): - @awaiter - async def test_connect_close(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - conn = ConnectionAsync() - await conn.connect() - self.assertIsNotNone(conn.reader) - self.assertIsNotNone(conn.writer) - - await conn.close() - self.assertIsNone(conn.reader) - self.assertIsNone(conn.writer) - - @awaiter - async def test_connect_close_catch_oserror(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - conn = ConnectionAsync() - await conn.connect() - self.assertIsNotNone(conn.reader) - self.assertIsNotNone(conn.writer) - - with patch('{}.FakeStreamWriter.close'.format(__name__), side_effect=OSError): - await conn.close() - self.assertIsNone(conn.reader) - self.assertIsNone(conn.writer) - - @awaiter - async def test_connect_with_timeout(self): - with self.assertRaises(RuntimeError): - with async_patch('asyncio.open_connection', side_effect=asyncio.TimeoutError): - conn = ConnectionAsync(timeout=1) - await conn.connect() - - -if __name__ == '__main__': - unittest.main() diff --git a/test_async/test_device_async.py b/test_async/test_device_async.py deleted file mode 100644 index b6affd4..0000000 --- a/test_async/test_device_async.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Unit tests for the `DeviceAsync` class. - -""" - - -import asyncio -from contextlib import asynccontextmanager -import os -import sys -import unittest -from unittest.mock import mock_open, patch - -sys.path.insert(0, '..') - -from ppadb.client_async import ClientAsync -from ppadb.protocol import Protocol -from ppadb.sync_async import SyncAsync - -from .async_wrapper import awaiter -from .patchers import FakeStreamReader, FakeStreamWriter, async_mock_open, async_patch - - -PNG_IMAGE = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\n\x00\x00\x00\n\x08\x06\x00\x00\x00\x8d2\xcf\xbd\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\tpHYs\x00\x00\x0fa\x00\x00\x0fa\x01\xa8?\xa7i\x00\x00\x00\x0eIDAT\x18\x95c`\x18\x05\x83\x13\x00\x00\x01\x9a\x00\x01\x16\xca\xd3i\x00\x00\x00\x00IEND\xaeB`\x82' - -PNG_IMAGE_NEEDS_REPLACING = PNG_IMAGE[:5] + b'\r' + PNG_IMAGE[5:] - -FILEDATA = b'Ohayou sekai.\nGood morning world!' - - -class TestDevice(unittest.TestCase): - @awaiter - async def setUp(self): - self.client = ClientAsync() - - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'000b', b'serial test']): - self.device = await self.client.device('serial') - - @awaiter - async def test_shell(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'test', b'', b'OKAY']): - self.assertEqual(await self.device.shell('TEST'), 'test') - - @awaiter - async def test_shell_error(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), return_value=b'FAIL'): - with self.assertRaises(RuntimeError): - self.assertEqual(await self.device.shell('TEST'), 'test') - - @awaiter - async def test_screencap(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', PNG_IMAGE, b'', b'OKAY']): - self.assertEqual(await self.device.screencap(), PNG_IMAGE) - - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', PNG_IMAGE_NEEDS_REPLACING, b'', b'OKAY']): - self.assertEqual(await self.device.screencap(), PNG_IMAGE) - - @awaiter - async def test_push_file_not_found(self): - with patch('os.path.exists', return_value=False): - with self.assertRaises(FileNotFoundError): - await self.device.push('src', 'dest') - - with self.assertRaises(FileNotFoundError): - sync = SyncAsync('Unused') - await sync.push('src', 'dest', 'mode') - - @awaiter - async def test_push(self): - def progress(*args, **kwargs): - pass - - filedata = b'Ohayou sekai.\nGood morning world!' - with patch('os.path.exists', return_value=True), patch('os.path.isfile', return_value=True), patch('os.stat', return_value=os.stat_result((123,) * 10)), patch('ppadb.sync_async.aiofiles.open', async_mock_open(FILEDATA)): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'OKAY', PNG_IMAGE_NEEDS_REPLACING, b'', b'OKAY']): - await self.device.push('src', 'dest', progress=progress) - - @awaiter - async def test_push_dir(self): - with patch('os.path.exists', return_value=True), patch('os.path.isfile', return_value=False), patch('os.path.isdir', return_value=True), patch('os.walk', return_value=[('root1', 'dirs1', 'files1'), ('root2', 'dirs2', 'files2')]): - with async_patch('ppadb.device_async.DeviceAsync.shell'), async_patch('ppadb.device_async.DeviceAsync._push'): - await self.device.push('src', 'dest') - - @awaiter - async def test_pull(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'DATA', SyncAsync._little_endian(4), b'TEST', b'DONE', b'OKAY']): - with patch('ppadb.sync_async.aiofiles.open', async_mock_open(FILEDATA)): - await self.device.pull('src', 'dest') - - @awaiter - async def test_pull_fail(self): - with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): - with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'FAIL', SyncAsync._little_endian(4), b'TEST', b'DONE', b'OKAY']): - with patch('ppadb.sync_async.aiofiles.open', async_mock_open(FILEDATA)): - await self.device.pull('src', 'dest') - - -class TestProtocol(unittest.TestCase): - def test_encode_decode_length(self): - for i in range(16 ** 2): - self.assertEqual(i, Protocol.decode_length(Protocol.encode_length(i))) - - -if __name__ == '__main__': - unittest.main() diff --git a/ppadb/utils/__init__.py b/utils/__init__.py similarity index 100% rename from ppadb/utils/__init__.py rename to utils/__init__.py diff --git a/utils/__pycache__/__init__.cpython-38.pyc b/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..3137647 Binary files /dev/null and b/utils/__pycache__/__init__.cpython-38.pyc differ diff --git a/utils/__pycache__/logger.cpython-38.pyc b/utils/__pycache__/logger.cpython-38.pyc new file mode 100644 index 0000000..a4a3b5c Binary files /dev/null and b/utils/__pycache__/logger.cpython-38.pyc differ diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..15448d2 --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,12 @@ +import logging +from logging.config import fileConfig + +logging.getLogger("ppadb").setLevel(logging.CRITICAL) + +class AdbLogging: + + @classmethod + def get_logger(cls, name): + fileConfig('logging_config.ini') + logger = logging.getLogger() + return logger