diff --git a/CHANGELOG.md b/CHANGELOG.md index aa77da5..2fa7343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/adb_pywrapper/adb_device.py b/adb_pywrapper/adb_device.py index e642668..6af5713 100644 --- a/adb_pywrapper/adb_device.py +++ b/adb_pywrapper/adb_device.py @@ -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 @@ -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} ' @@ -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 @@ -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] @@ -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]: @@ -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]: @@ -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]: @@ -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] @@ -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: @@ -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) @@ -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 @@ -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: """ @@ -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: """ @@ -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}') diff --git a/setup.py b/setup.py index 20a2b5c..38b18e3 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="adb_pywrapper", - version="1.0.1", + version="1.0.2", packages=find_packages(), test_suite="test", diff --git a/test/test_adb_device.py b/test/test_adb_device.py new file mode 100644 index 0000000..87b9436 --- /dev/null +++ b/test/test_adb_device.py @@ -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() diff --git a/test/test_adb_pywrapper.py b/test/test_adb_pywrapper.py index 661125a..a1ea4ec 100644 --- a/test/test_adb_pywrapper.py +++ b/test/test_adb_pywrapper.py @@ -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. @@ -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 @@ -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)