async def from_apk_async(cls: Type[_Ty], apk_path: str, device: Device, as_upgrade=False) -> _Ty: """ Install provided application asynchronously. This allows the output of the install to be processed in a streamed fashion. This can be useful on some devices that are non-standard android where installs cause (for example) a pop-up requesting direct user permission for the install -- monitoring for a specific message to simulate the tap to confirm the install. :param apk_path: path to apk :param device: device to install on :param as_upgrade: whether to install as upgrade or not :return: remote installed application :raises: Exception if failure ot install or verify installation >>> async def install(): ... async with await Application.from_apk_async("/some/local/path/to/apk") as stdout: ... async for line in stdout: ... if "some trigger message" in line: ... perform_tap_to_accept_install() """ parser = AXMLParser.parse(apk_path) package = parser.package_name await device.install(apk_path, as_upgrade) return cls(package, device)
def apk_info( apk_file_name: str) -> Tuple[Optional[Any], Optional[Any]]: attrs = { attr.name: attr.value for attr in AXMLParser.parse(apk_file_name).xml_head.attributes } return attrs.get('package'), attrs.get('versionName')
def from_apk(cls, apk_path: str, device: Device, as_upgrade=False) -> "TestApplication": """ install apk as a test application on the given device :param apk_path: path to test apk (containing a runner) :param device: device to install on :param as_upgrade: whether to install as upgrade or not :return: `TestApplication` of installed apk >>> test_application = Application.from_apk("/local/path/to/apk", device, as_upgrade=True) """ parser = AXMLParser.parse(apk_path) # The manifest of the package should contain an instrumentation section, if not, it is not # a test apk: valid = (hasattr(parser, "instrumentation") and (parser.instrumentation is not None) and bool(parser.instrumentation.target_package) and bool(parser.instrumentation.runner)) if not valid: raise Exception( "Test application's manifest does not specify proper instrumentation element." "Are you sure this is a test app") device.install_synchronous(apk_path, as_upgrade) return cls(parser.package_name, device, parser.instrumentation.target_package, parser.instrumentation.runner)
async def install(self, apk_path: str, as_upgrade: bool, conditions: List[str] = None, on_full_install: Optional[Callable] = None) -> None: """ Install given apk asynchronously, monitoring output for messages containing any of the given conditions, executing a callback if given when any such condition is met :param apk_path: bundle to install :param as_upgrade: whether as upgrade or not :param conditions: list of strings to look for in stdout as a trigger for callback Some devices are non-standard and will provide a pop-up request explicit user permission for install once the apk is fully uploaded and prepared This param defaults to ["100\%", "pkg:", "Success"] as indication that bundle was fully prepared (pre-pop-up) :param on_full_install: if not None the callback to be called :raises: Device.InsufficientStorageError if there is not enough space on device """ conditions = conditions or ["100\%", "pkg:", "Success"] parser = AXMLParser.parse(apk_path) package = parser.package_name if not as_upgrade: # avoid unnecessary conflicts with older certs and crap: # TODO: client should handle clean install -- this code really shouldn't be here?? with suppress(Exception): self.execute_remote_cmd("uninstall", package, capture_stdout=False) # Execute the installation of the app, monitoring output for completion in order to invoke # any extra commands if as_upgrade: cmd = ("install", "-r", apk_path) else: cmd = ("install", apk_path) # do not allow more than one install at a time on a specific device, as # this can be problematic async with self.lock(): async with await self.execute_remote_cmd_async( *cmd, proc_completion_timeout=TIMEOUT_LONG_ADB_CMD) as lines: async for msg in lines: log.debug(msg) if self.ERROR_MSG_INSUFFICIENT_STORAGE in msg: raise self.InsufficientStorageError( "Insufficient storage for install of %s" % apk_path) # some devices have non-standard pop-ups that must be cleared by accepting usb installs # (non-standard Android): if on_full_install and msg and any( [condition in msg for condition in conditions]): on_full_install() # on some devices, a pop-up may prevent successful install even if return code from adb install showed # success, so must explicitly verify the install was successful: log.debug("Verifying install...") self._verify_install(apk_path, package) # raises exception on failure to verify
def uninstall_apk(apk, device): """ A little of a misnomer, as we don't actually uninstall an apk, however we can get the name of the package from the apk and ensure the app is not installed on the device (ensure "cleanliness" for testing) :param apk: apk to get package name from :param device: device to uninstall package from """ with suppress(Exception): Application(AXMLParser.parse(apk).package_name, device).uninstall()
def test_uninstall_upgrade(self, upgrade_apk: str) -> None: package = AXMLParser.parse(upgrade_apk).package_name if package not in self._device.list_installed_packages(): return app = Application(self._device, {'package_name': package}) app.stop() app.uninstall() if package in self._device.list_installed_packages(): raise UpgradeTestException( f"Uninstall upgrade package {package} failed")
def from_apk( cls: Type[_TTestApp], apk_path: str, device: Device, as_upgrade: bool = False, timeout: Optional[int] = Device.TIMEOUT_LONG_ADB_CMD) -> _TTestApp: parser = AXMLParser.parse(apk_path) args = ["-t"] if not as_upgrade else ["-r", "-t"] cls._install(device, apk_path, *args, timeout=timeout) return cls(device, parser)
def test_apk_later_version(self): parser = AXMLParser.parse(USER_ACCEPTANCE_APK) assert { 'android.permission.ACCESS_FINE_LOCATION', 'android.permission.MANAGE_ACCOUNTS', 'android.permission.WRITE_SYNC_SETTINGS', 'android.permission.INTERNET', 'android.permission.READ_SYNC_SETTINGS', 'android.permission.AUTHENTICATE_ACCOUNTS', } < set(parser.permissions)
def test_apk_parsing(self): parser = AXMLParser.parse(BASIC_APK) assert str(parser.xml) == EXPECTED_XML assert parser.package_name == "com.linkedin.mdctest.test" assert parser.instrumentation is not None assert parser.instrumentation.handle_profiling is False assert parser.instrumentation.functional_test is False assert parser.instrumentation.runner == "android.support.test.runner.AndroidJUnitRunner" assert parser.instrumentation.label == "Tests for com.linkedin.mdctest" assert parser.instrumentation.target_package == "com.linkedin.mdctest" assert parser.uses_sdk.min_sdk_version == 15 assert parser.uses_sdk.target_sdk_version == 25 assert not parser.permissions # no permissions
async def _monitor_install( cls, device: Device, apk_path: str, *args: str, timeout: Optional[int] = Device.TIMEOUT_LONG_ADB_CMD, callback: Optional[Callable[[Device.Process], None]] = None) -> None: """ Install given apk asynchronously, monitoring output for messages containing any of the given conditions, executing a callback if given when any such condition is met. :param device: Device to install on :param apk_path: bundle to install :param timeout: timeout if install takes too long :param callback: if not None, callback to be made on successful push and at start of install; the `Device.Process` that is installing from device storage is passed as the only parameter to the callback :raises Device.InsufficientStorageError: if there is not enough space on device :raises IOError if push of apk to device was unsuccessful :raises; TimeoutError if install takes more than timeout parameter """ parser = AXMLParser.parse(apk_path) package = parser.package_name remote_data_path = f"/data/local/tmp/{package}" try: # We try Android Studio's method of pushing to device and installing from there, but if push is # unsuccessful, we fallback to plain adb install push_cmd = ("push", apk_path, remote_data_path) device.execute_remote_cmd(*push_cmd, timeout=timeout) # Execute the installation of the app, monitoring output for completion in order to invoke any extra # commands or detect insufficient storage issues cmd: List[str] = ["shell", "pm", "install" ] + list(args) + [remote_data_path] # Do not allow more than one install at a time on a specific device, as this can be problematic async with _device_lock(device), device.monitor_remote_cmd( *cmd) as proc: if callback: callback(proc) await proc.wait(timeout=Device.TIMEOUT_LONG_ADB_CMD) except Device.CommandExecutionFailure as e: raise IOError(f"Unable to push apk to device {e}") from e finally: with suppress(Exception): rm_cmd = ("shell", "rm", remote_data_path) device.execute_remote_cmd(*rm_cmd, timeout=Device.TIMEOUT_ADB_CMD)
async def from_apk_async( cls: Type[_TTestApp], apk_path: str, device: Device, as_upgrade: bool = False, timeout: Optional[int] = Device.TIMEOUT_LONG_ADB_CMD, callback: Optional[Callable[[Device.Process], None]] = None) -> _TTestApp: parser = AXMLParser.parse(apk_path) args = ["-t"] if not as_upgrade else ["-r", "-t"] await cls._monitor_install(device, apk_path, parser.package_name, *args, callback=callback, timeout=timeout) return cls(device, parser)
def from_apk(cls: Type[_Ty], apk_path: str, device: Device, as_upgrade=False) -> _Ty: """ Install provided application, blocking until install is complete :param apk_path: path to apk :param device: device to install on :param as_upgrade: whether to install as upgrade or not :return: remote installed application :raises: Exception if failure ot install or verify installation """ parser = AXMLParser.parse(apk_path) package = parser.package_name device.install_synchronous(apk_path, as_upgrade) return cls(package, device)
async def from_apk_async( cls: Type[_TApp], apk_path: str, device: Device, as_upgrade: bool = False, timeout: Optional[int] = Device.TIMEOUT_LONG_ADB_CMD, callback: Optional[Callable[[Device.Process], None]] = None) -> _TApp: """ Install provided application asynchronously. This allows the output of the install to be processed in a streamed fashion. This can be useful on some devices that are non-standard android where installs cause (for example) a pop-up requesting direct user permission for the install -- monitoring for a specific message to simulate the tap to confirm the install. :param apk_path: path to apk :param device: device to install on :param as_upgrade: whether to install as upgrade or not :param timeout: raises TimeoutError if specified and install takes too long :param callback: if not None, callback to be made on successful push and at start of install; the `Device.Process` that is installing from device storage is passed as the only parameter to the callback :return: remote installed application :raises: Exception if failure of install or verify installation :raises: TimeoutError if install takes more than timeout parameter >>> async def install(): ... async with Application.from_apk_async("/some/local/path/to/apk", device) as stdout: ... async for line in stdout: ... if "some trigger message" in line: ... print(line) """ parser = AXMLParser.parse(apk_path) args = ["-r"] if as_upgrade else [] await cls._monitor_install(device, apk_path, *args, callback=callback, timeout=timeout) return cls(device, parser)
def from_apk( cls: Type[_TApp], apk_path: str, device: Device, as_upgrade: bool = False, timeout: Optional[int] = Device.TIMEOUT_LONG_ADB_CMD) -> _TApp: """ Install provided application, blocking until install is complete :param apk_path: path to apk :param device: device to install on :param as_upgrade: whether to install as upgrade or not :param timeout: raises TimeoutError if specified and install takes too long :return: remote installed application :raises: Exception if failure ot install or verify installation :raises: TimeoutError if install takes more than timeout parameter >>> app = Application.from_apk("/local/path/to/apk", device, as_upgrade=True) """ parser = AXMLParser.parse(apk_path) args = ["-r"] if as_upgrade else [] cls._install(device, apk_path, *args, timeout=timeout) return cls(device, parser)
def __init__(self, device: Device, apk_under_test: str): self._device = device self._apk_under_test = apk_under_test self._package_name = AXMLParser.parse( self._apk_under_test).package_name
def test_apk_with_permissions(self): parser = AXMLParser.parse(COMPLEX_APK) assert set(parser.permissions) == { "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE" }