Skip to content

Commit

Permalink
Merge pull request #22 from NetherlandsForensicInstitute/20-pull-file…
Browse files Browse the repository at this point in the history
…s-where-there-is-a-space-in-the-folder-name

20 correctly handle spaces in paths, add test, fix release notes
  • Loading branch information
btimbermont authored Nov 19, 2024
2 parents 380007e + 4229f09 commit 3e7bf8e
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 51 deletions.
82 changes: 55 additions & 27 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,55 @@
```
v1.0.0 - Change name to adb-pywrapper and add action to release to PyPi
```
```
v0.8 - Removed lru_cache function that caused get_prop output to be cached which is not advisable since these properties can change
```
```
v0.7 - Improved the documentation of all the functions and the README.md
```
```
v0.6 - Introduced get_device_status functionality to get the device status using a static function
```
```
v0.5 - Introduced snapshot_list, load, save and delete for emulator snapshot interaction
```
```
v0.4 - Introduced get-state function to get the current status of a connected device. Example: device, offline, fastboot
```
```
v0.3 - Introduced emu avd function for emulator communication trough adb
```
```
v0.2 - Introduced get_prop function from OSS project
```
```
v0.1 - Initial release based on ADB functions introduced in the Appium project
```
1.0.2
-----

- 20: correctly pull files when name contains a space

1.0.1
-----

- implemented optionally specified wait time for wait_for_device functionality

1.0.0
-----

- Change name to adb-pywrapper and add action to release to PyPi

0.8
---

- Removed lru_cache function that caused get_prop output to be cached which is not advisable since these properties can
change

0.7
---

- Improved the documentation of all the functions and the README.md

0.6
---

- Introduced get_device_status functionality to get the device status using a static function

0.5
---

- Introduced snapshot_list, load, save and delete for emulator snapshot interaction

0.4
---

- Introduced get-state function to get the current status of a connected device. Example: device, offline, fastboot

0.3
---

- Introduced emu avd function for emulator communication trough adb

0.2
---

- Introduced get_prop function from OSS project

0.1
---

- Initial release based on ADB functions introduced in the Appium project
43 changes: 22 additions & 21 deletions adb_pywrapper/adb_device.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import subprocess
from os import makedirs
from os.path import basename, isfile
from shlex import quote
from subprocess import CompletedProcess
from time import sleep
from typing import Optional
Expand All @@ -17,8 +18,8 @@ def __init__(self, device: str = None, check_device_exists: bool = True):
connected_devices = AdbDevice.list_devices()
if device not in connected_devices:
log_error_and_raise_exception(logger,
f'Cannot create adb connection with device `{device}` '
f'as it cannot be found with `adb devices`: {connected_devices}')
f'Cannot create adb connection with device `{device}` '
f'as it cannot be found with `adb devices`: {connected_devices}')
self.device_command = ''
if device is not None:
self.device_command += f'-s {device} '
Expand Down Expand Up @@ -60,7 +61,7 @@ def list_devices() -> list[str]:
result = AdbDevice._adb_command('devices')
if not result.success:
log_error_and_raise_exception(logger, f'Could not get list of available adb devices. '
f'ADB output: {result.stdout}{result.stderr}')
f'ADB output: {result.stdout}{result.stderr}')
devices = [line[:line.index('\t')] for line in result.stdout.splitlines() if '\t' in line]
return devices

Expand All @@ -74,7 +75,7 @@ def get_device_status(device_name) -> str:
result = AdbDevice._adb_command('devices')
if not result.success:
log_error_and_raise_exception(logger, f'Could not get list of available adb devices. '
f'ADB output: {result.stdout}{result.stderr}')
f'ADB output: {result.stdout}{result.stderr}')
for line in result.stdout.splitlines():
if line.startswith(device_name):
return line.split('\t')[1]
Expand Down Expand Up @@ -141,11 +142,11 @@ def ls(self, path: str) -> Optional[list[str]]:
:param path: the path on the device
:return: a list containing the contents of the given path
"""
adb_result = self.shell(f'ls {path}')
adb_result = self.shell(f'ls {quote(path)}')
if not adb_result.success:
log_error_and_raise_exception(logger,
f'Could not get contents of path {path} on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
f'Could not get contents of path {path} on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
return adb_result.stdout.splitlines()

def installed_packages(self) -> list[str]:
Expand All @@ -156,8 +157,8 @@ def installed_packages(self) -> list[str]:
adb_result = self.shell(f'pm list packages')
if not adb_result.success:
log_error_and_raise_exception(logger,
f'Could not get installed packages on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
f'Could not get installed packages on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
return [line[line.index(':') + 1:] for line in adb_result.stdout.splitlines() if line.startswith('package:')]

def path_package(self, package_name: str) -> list[str]:
Expand All @@ -172,8 +173,8 @@ def path_package(self, package_name: str) -> list[str]:
adb_result = self.shell(f'pm path {package_name}')
if not adb_result.success:
log_error_and_raise_exception(logger,
f'Could not locate package {package_name} on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
f'Could not locate package {package_name} on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
return [line[line.index(':') + 1:] for line in adb_result.stdout.splitlines() if line.startswith('package:')]

def package_versions(self, package_name: str) -> list[str]:
Expand All @@ -186,8 +187,8 @@ def package_versions(self, package_name: str) -> list[str]:
adb_result = self.shell(f"dumpsys package {package_name} | grep versionName")
if not adb_result.success:
log_error_and_raise_exception(logger,
f'Could not locate package {package_name} on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
f'Could not locate package {package_name} on device {self.device}. '
f'adb stderr: {adb_result.stderr}')
result = adb_result.stdout.splitlines()
return [line.split("=")[-1] for line in result]

Expand All @@ -202,7 +203,7 @@ def _pull(self, remote: str, local: Optional[str] = None, a: bool = False) -> Ad
command = 'pull'
if a:
command += ' -a'
adb_result = self._command(f'{command} {remote}{f" {local}" if local else ""}')
adb_result = self._command(f'{command} {quote(remote)}{f" {quote(local)}" if local else ""}')
return adb_result

def pull(self, file_to_pull: str, destination: str) -> PullResult:
Expand All @@ -222,8 +223,8 @@ def pull(self, file_to_pull: str, destination: str) -> PullResult:
sleep(1)
if not pull_result.success:
log_error_and_raise_exception(logger,
f'Could not pull file {file_to_pull} on device {self.device}, '
f'adb output: {pull_result.stdout}{pull_result.stderr}')
f'Could not pull file {file_to_pull} on device {self.device}, '
f'adb output: {pull_result.stdout}{pull_result.stderr}')

result_file_path = f'{destination}/{basename(file_to_pull)}'
return PullResult(result_file_path, pull_result)
Expand All @@ -243,8 +244,8 @@ def pull_package(self, package_name: str, destination: str) -> list[PullResult]:
files_to_pull = self.path_package(package_name)
if len(files_to_pull) == 0:
log_error_and_raise_exception(logger,
f'Could not locate any package files for package {package_name} on '
f'device {self.device}. Is it installed on the device?')
f'Could not locate any package files for package {package_name} on '
f'device {self.device}. Is it installed on the device?')
for file_to_pull in files_to_pull:
result.append(self.pull(file_to_pull, destination))
return result
Expand All @@ -260,7 +261,7 @@ def install(self, apk_path: str, r: bool = False) -> AdbResult:
command = 'install'
if r:
command += ' -r'
return self._command(f'{command} {apk_path}')
return self._command(f'{command} {quote(apk_path)}')

def install_multiple(self, apk_paths: [str], r: bool = False) -> AdbResult:
"""
Expand All @@ -274,7 +275,7 @@ def install_multiple(self, apk_paths: [str], r: bool = False) -> AdbResult:
command = 'install-multiple'
if r:
command += ' -r'
return self._command(f'{command} {" ".join(apk_paths)}')
return self._command(f'{command} {" ".join([quote(path) for path in apk_paths])}')

def open_intent(self, url: str) -> AdbResult:
"""
Expand Down Expand Up @@ -307,7 +308,7 @@ def _snapshot_command(self, subcommand: str, snapshot_name: Optional[str] = None
allowed_subcommands = ["list", "save", "load", "del", "get"]
if not subcommand in allowed_subcommands:
log_error_and_raise_exception(logger,
f"Could not execute snapshot subcommand {subcommand}, should be one of {', '.join(allowed_subcommands)}")
f"Could not execute snapshot subcommand {subcommand}, should be one of {', '.join(allowed_subcommands)}")
if subcommand not in ["list", "get"] and snapshot_name is None:
logger.warning(logger, f"Snapshot subcommand requires a snapshot_name, None is given.")
return self.emulator_emu_avd(f' snapshot {subcommand} {snapshot_name}')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name="adb_pywrapper",
version="1.0.1",
version="1.0.2",
packages=find_packages(),
test_suite="test",

Expand Down
78 changes: 78 additions & 0 deletions test/test_adb_device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os.path
import shutil
import unittest
from os.path import basename
from pathlib import Path
from subprocess import CompletedProcess
from unittest.mock import patch, Mock

from parameterized import parameterized

from adb_pywrapper.adb_device import AdbDevice
from adb_pywrapper.adb_result import AdbResult


class MockAdbResult(AdbResult):
def __init__(self, stdout: str = '', stderr: str = '', success: bool = True):
self.completed_adb_process = Mock(CompletedProcess)
self.stdout = stdout
self.stderr = stderr
self.success = success


class TestAdbDevice(unittest.TestCase):

@parameterized.expand([
('/sdcard/test.txt', './foo', 'pull /sdcard/test.txt ./foo'),
('/sdcard/test file.txt', './foo', 'pull \'/sdcard/test file.txt\' ./foo'),
('/sdcard/test.txt', './foo bar', 'pull /sdcard/test.txt \'./foo bar\''),
('/sdcard/test file.txt', './foo bar', 'pull \'/sdcard/test file.txt\' \'./foo bar\'')
])
@patch('adb_pywrapper.adb_device.AdbDevice._adb_command')
def test_pull(self, file_to_pull, destination, expected_command, mock_adb_command):
try:
device = AdbDevice()
result = device.pull(file_to_pull, destination)

mock_adb_command.assert_called_once_with(expected_command, None)
self.assertTrue(os.path.exists(destination)) # destination folder should have bene created
self.assertFalse(
result.success) # this is set to True if the file has been copied to the destination. We didn't copy anything
expected_result_path = Path(destination) / basename(file_to_pull)
self.assertEqual(f'./{str(expected_result_path)}', result.path)
finally:
shutil.rmtree(destination)

@patch('adb_pywrapper.adb_device.AdbDevice._adb_command')
def test_ls(self, mock_adb_command):
device = AdbDevice()
# happy flow
mock_adb_command.return_value = MockAdbResult('foo.txt\nbar.txt', '')
result = device.ls('/sdcard')
mock_adb_command.assert_called_once_with('shell ls /sdcard', None)
self.assertEqual(['foo.txt', 'bar.txt'], result)
# escape special characters
device.ls('/sdcard/test folder')
mock_adb_command.assert_called_with('shell ls \'/sdcard/test folder\'', None)

@patch('adb_pywrapper.adb_device.AdbDevice._adb_command')
def test_install(self, mock_adb_command):
device = AdbDevice()
result = device.install('/path/to/apk')
mock_adb_command.assert_called_once_with('install /path/to/apk', None)
result = device.install('/path/to/other apk')
mock_adb_command.assert_called_with('install \'/path/to/other apk\'', None)
result = device.install('/path/to/yet another apk', True)
mock_adb_command.assert_called_with('install -r \'/path/to/yet another apk\'', None)

@patch('adb_pywrapper.adb_device.AdbDevice._adb_command')
def test_install_multiple(self, mock_adb_command):
device = AdbDevice()
result = device.install_multiple(['foo.apk', 'bar.apk'])
mock_adb_command.assert_called_once_with('install-multiple foo.apk bar.apk', None)
result = device.install_multiple(['foo bar.apk', 'bar foo.apk'])
mock_adb_command.assert_called_with("install-multiple 'foo bar.apk' 'bar foo.apk'", None)


if __name__ == '__main__':
unittest.main()
9 changes: 7 additions & 2 deletions test/test_adb_pywrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ def mock_adb_call(arguments_string, timeout: int = None):


class TestAdb(unittest.TestCase):
"""
DEPRECATED CLASS
All new tests should use the unittest framework with patching, in test_adb_device.py
Existing tests should be moved, as per issue #21
"""

def setUp(self) -> None:
# For testing we create an AdbDevice in which we mock the _adb_command() method.
Expand Down Expand Up @@ -181,7 +186,7 @@ def test_package_versions(self):

def test_pull(self):
success_result = MockAdbResult(
stdout='pull successfull! (we don\'t stdout so I can put anything I want here. poop.)')
stdout='pull successfull! (we don\'t parse stdout so I can put anything I want here)')
failure_result = MockAdbResult(stderr='adb gods are angry', success=False)

# pull goes well: we 'pull' a file to this exact location to pretend the pull was successful
Expand All @@ -192,7 +197,7 @@ def test_pull(self):
# pull goes well according to adb, but the resulting file is not present so the result should be a failure
pull_result = self.device.pull('bla', 'folder_for_testing_purposes')
self.assertFalse(pull_result.success)
self.assertTrue(os.path.isdir('folder_for_testing_purposes')) # destination fodler should be created
self.assertTrue(os.path.isdir('folder_for_testing_purposes')) # destination folder should be created
os.rmdir('folder_for_testing_purposes')
# pull goes wrong
self._mock_adb_results(failure_result)
Expand Down

0 comments on commit 3e7bf8e

Please sign in to comment.