diff --git a/.gitignore b/.gitignore index f7d021e..88b32b4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ dist .pytest_cache *.pyc venv +.venv test_result.xml -.python-version \ No newline at end of file +.python-version +build/ diff --git a/ppadb/device.py b/ppadb/device.py index ca0cb1a..a7d7dd4 100644 --- a/ppadb/device.py +++ b/ppadb/device.py @@ -73,13 +73,40 @@ def push(self, src, dest, mode=0o644, progress=None): for item in files: self._push(root / item, destdir / item, mode, progress) - def pull(self, src, dest): + def _pull(self, src, dest): sync_conn = self.sync() sync = Sync(sync_conn) with sync_conn: return sync.pull(src, dest) + def pull(self, src, dest): + src = PurePosixPath(src) + dest = Path(dest) + + dir_string = "IS_DIR" + res = self.shell(f"[ -d {src} ] && echo {dir_string}") + if dir_string in res: + # Get all files in the dir + # Pull each + dest.mkdir(exist_ok=True) + cmd = f"ls -1a {src}" + res = self.shell(cmd) + contents_list = res.split("\n") + contents_list = [ + x for x in contents_list if x != "." and x != ".." and x != "" + ] + for element in contents_list: + element_src = src / element + element_dest = dest / element + self.pull(element_src, element_dest) + else: + file_string = "IS_FILE" + res = self.shell(f"[ -f {src} ] && echo {file_string}") + if file_string not in res: + raise FileNotFoundError(f"Cannot find {src} on device") + self._pull(str(src), str(dest)) + def install( self, path, diff --git a/ppadb/device_async.py b/ppadb/device_async.py index 41b517c..26c23de 100644 --- a/ppadb/device_async.py +++ b/ppadb/device_async.py @@ -58,13 +58,40 @@ async def push(self, src, dest, mode=0o644, progress=None): for item in files: await self._push(root / item, destdir / item, mode, progress) - async def pull(self, src, dest): + 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) + async def pull(self, src, dest): + src = PurePosixPath(src) + dest = Path(dest) + + dir_string = "IS_DIR" + res = await self.shell(f"[ -d {src} ] && echo {dir_string}") + if dir_string in res: + # Get all files in the dir + # Pull each + dest.mkdir(exist_ok=True) + cmd = f"ls -1a {src}" + res = await self.shell(cmd) + contents_list = res.split("\n") + contents_list = [ + x for x in contents_list if x != "." and x != ".." and x != "" + ] + for element in contents_list: + element_src = src / element + element_dest = dest / element + await self.pull(element_src, element_dest) + else: + file_string = "IS_FILE" + res = await self.shell(f"[ -f {src} ] && echo {file_string}") + if file_string not in res: + raise FileNotFoundError(f"Cannot find {src} on device") + await self._pull(str(src), str(dest)) + async def install( self, path, diff --git a/test/conftest.py b/test/conftest.py index 6644206..cc0f209 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,8 +9,6 @@ logger = logging.getLogger(__name__) -# adb_host = "emulator" -# adb_host = "172.20.0.2" adb_host = os.environ.get("ADB_HOST", "127.0.0.1") adb_port = int(os.environ.get("ADB_PORT", "5037")) device_serial = os.environ.get("DEVICE_SERIAL", "emulator-5554") @@ -99,7 +97,7 @@ def emulator_console_is_connectable(): try: console = EmulatorConsole(host=adb_host, port=emulator_port) return console - except Exception as e: + except Exception: return None def is_boot_completed(): diff --git a/test/test_device.py b/test/test_device.py index 78d10be..0839c0d 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -1,5 +1,7 @@ import os import time +import tempfile +import pathlib import pytest import socket @@ -181,6 +183,79 @@ def progress(file_name, total_size, sent_size): assert result[-1]["total_size"] == result[-1]["sent_size"] +@pytest.fixture(scope="function") +def test_filepaths(): + filepaths = [ + "toplevel/test1.txt", + "toplevel/test2.txt", + "toplevel/subdir1/test3.txt", + "toplevel/subdir1/test4.txt", + "toplevel/subdir1/subdir2/test5.txt", + "toplevel/subdir1/subdir2/test6.txt", + ] + yield filepaths + + +@pytest.fixture(scope="function") +def populated_device(device, test_filepaths): + dirpath = "toplevel/subdir1/subdir2" + + device.shell(f"mkdir -p /data/local/tmp/{dirpath}") + for path in test_filepaths: + device.shell(f"echo {path} > /data/local/tmp/{path}") + + yield device + + device.shell("rm -rf /data/local/tmp/toplevel") + + +@pytest.fixture(scope="function") +def working_dir(): + with tempfile.TemporaryDirectory() as f: + yield pathlib.Path(f) + + +def test_pull_file(populated_device, working_dir): + populated_device.pull( + "/data/local/tmp/toplevel/test1.txt", working_dir / "test1.txt" + ) + dest_path = working_dir / "test1.txt" + assert dest_path.is_file() + assert dest_path.read_text() == "toplevel/test1.txt\n" + + +def test_pull_dir(populated_device, working_dir): + populated_device.pull( + "/data/local/tmp/toplevel/subdir1/subdir2", working_dir / "subdir2" + ) + dest_path = working_dir / "subdir2" + filepath1 = dest_path / "test5.txt" + filepath2 = dest_path / "test6.txt" + assert dest_path.is_dir() + assert filepath1.is_file() + assert filepath1.read_text() == "toplevel/subdir1/subdir2/test5.txt\n" + assert filepath2.is_file() + assert filepath2.read_text() == "toplevel/subdir1/subdir2/test6.txt\n" + + +def test_pull_recursive_dir(populated_device, working_dir, test_filepaths): + populated_device.pull("data/local/tmp/toplevel", working_dir / "toplevel") + assert (working_dir / "toplevel").is_dir() + assert (working_dir / "toplevel" / "subdir1").is_dir() + for path in test_filepaths: + to_check = working_dir / path + assert to_check.is_file() + assert to_check.read_text() == f"{path}\n" + + +def test_pull_nonexisting(device, working_dir): + src = "data/local/tmp/dklsfalkjhvcnsdfalvfds" + with pytest.raises(Exception) as excinfo: + device.pull(src, working_dir / "non_existent") + + assert str(excinfo.value) == f"Cannot find {src} on device" + + def test_forward(device): device.killforward_all() forward_map = device.list_forward() diff --git a/test_async/test_client_async.py b/test_async/test_client_async.py index f200909..5980b10 100644 --- a/test_async/test_client_async.py +++ b/test_async/test_client_async.py @@ -1,6 +1,4 @@ -"""Unit tests for the `ClientAsync` class. - -""" +"""Unit tests for the `ClientAsync` class.""" import asyncio import sys diff --git a/test_async/test_connection_async.py b/test_async/test_connection_async.py index 87a1ed4..ffc5ab1 100644 --- a/test_async/test_connection_async.py +++ b/test_async/test_connection_async.py @@ -1,6 +1,4 @@ -"""Unit tests for `ConnectionAsync` class. - -""" +"""Unit tests for `ConnectionAsync` class.""" import asyncio import sys diff --git a/test_async/test_device_async.py b/test_async/test_device_async.py index 9cfc8b7..226f38a 100644 --- a/test_async/test_device_async.py +++ b/test_async/test_device_async.py @@ -1,14 +1,9 @@ -"""Unit tests for the `DeviceAsync` class. +"""Unit tests for the `DeviceAsync` class.""" -""" - -import asyncio -from contextlib import asynccontextmanager import os -import pathlib import sys import unittest -from unittest.mock import mock_open, patch +from unittest.mock import patch sys.path.insert(0, "..") @@ -144,6 +139,8 @@ async def test_pull(self): with async_patch( "asyncio.open_connection", return_value=(FakeStreamReader(), FakeStreamWriter()), + ), async_patch( + "ppadb.device_async.DeviceAsync.shell", return_value=["", "IS_FILE"] ): with async_patch( "{}.FakeStreamReader.read".format(__name__), @@ -165,6 +162,8 @@ async def test_pull_fail(self): with async_patch( "asyncio.open_connection", return_value=(FakeStreamReader(), FakeStreamWriter()), + ), async_patch( + "ppadb.device_async.DeviceAsync.shell", return_value=["", "IS_FILE"] ): with async_patch( "{}.FakeStreamReader.read".format(__name__),