From 486ade64996cb92220113329cc49f39a68a1cfb7 Mon Sep 17 00:00:00 2001 From: Sebastian Goscik Date: Wed, 7 Sep 2016 15:26:33 +0100 Subject: [PATCH] ApkWorkload: Reworked APK Resolution. APK Resolution is now handled a bit differently to try maximise the likelyhood of a workload running. Like before `force_install` will always try to install the host version, if it is not present or is not a correct version, it will error. `check_apk` has changed so that when it is `True` it will prefer to use the host side APK. If it is not there, or not a suitable version and/or abi and the target already has a correct version of the app, the target app will be used. When it is to `False` WA will prefer the target version of the app so long as it is a valid version, if it is not then it will fallback to the host side APK. --- doc/source/apk_workloads.rst | 57 ++++++++ doc/source/index.rst | 1 + wlauto/common/android/workload.py | 210 +++++++++++++++++++----------- 3 files changed, 195 insertions(+), 73 deletions(-) create mode 100644 doc/source/apk_workloads.rst diff --git a/doc/source/apk_workloads.rst b/doc/source/apk_workloads.rst new file mode 100644 index 00000000..5f5c8fab --- /dev/null +++ b/doc/source/apk_workloads.rst @@ -0,0 +1,57 @@ +.. _apk_workload_settings: + +APK Workloads +============= + +APK resolution +-------------- + +WA has various resource getters that can be configured to locate APK files but for most people APK files +should be kept in the ``$WA_HOME/dependencies/SOME_WORKLOAD/`` directory. (by default +``~/.workload_automation/dependencies/SOME_WORKLOAD/``). The ``WA_HOME`` enviroment variable can be used +to chnage the location of this folder. The APK files need to be put into the corresponding directories +for the workload they belong to. The name of the file can be anything but as explained below may need +to contain certain peices of information. + +All ApkWorkloads have parameters that affect the way in which APK files are resolved, ``check_abi``, +``force_install`` and ``check_apk``. Their exact behaviours are outlined below. + +.. confval:: check_abi + + If this setting is enabled WA's resource resolvers will look for the devices ABI within the file name + e.g. ``calculator_arm65.apk``. By default this setting is disabled since most apks will work across all + devices. You may wish to enable this feature when working with devices that support multiple ABI's (like + 64-bit devices that can run 32-bit APK files) and are specifically trying to test one or the other. + +.. confval:: force_install + + If this setting is enabled WA will *always* use the APK file on the host, and re-install it on every + iteration. If there is no APK on the host that is a suitable version and/or ABI for the workload WA + will error when ``force_install`` is enabled. + +.. confval:: check_apk + + This parameter is used to specify a preference over host or target versions of the app. When set to + ``True`` WA will prefer the host side version of the APK. It will check if the host has the APK and + if the host APK meets the version requirements of the workload. If does and the target already has + same version nothing will be done, other wise it will overwrite the targets app with the host version. + If the hosts is missing the APK or it does not meet version requirements WA will fall back to the app + on the target if it has the app and it is of a suitable version. When this parameter is set to + ``false`` WA will prefer to use the version already on the target if it meets the workloads version + requirements. If it does not it will fall back to search the host for the correct version. In both modes + if neither the host nor target have a suitable version, WA will error and not run the workload. + +Some workloads will also feature the follow parameters which will alter the way their APK files are resolved. + +.. confval:: version + + This parameter is used to specify which version of uiautomation for the workload is used. In some workloads + e.g. ``geekbench`` multiple versions with drastically different UI's are supported. When a workload uses a + version it is required for the APK file to contain the uiautomation version in the file name. In the case + of antutu the file names could be: ``geekbench_2.apk`` or ``geekbench_3.apk``. + +.. confval:: variant_name + + Some workloads use variants of APK files, this is usually the case with web browser APK files, these work + in exactly the same way as the version, the variant of the apk + diff --git a/doc/source/index.rst b/doc/source/index.rst index 46095f5d..4a50454b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -118,6 +118,7 @@ and detailed descriptions of how WA functions under the hood. additional_topics daq_device_setup revent + apk_workloads contributing API Reference diff --git a/wlauto/common/android/workload.py b/wlauto/common/android/workload.py index 9483eafd..8af9d1b5 100644 --- a/wlauto/common/android/workload.py +++ b/wlauto/common/android/workload.py @@ -25,7 +25,7 @@ from wlauto.core.workload import Workload from wlauto.core.resource import NO_ONE from wlauto.common.android.resources import ApkFile from wlauto.common.resources import ExtensionAsset, Executable, File -from wlauto.exceptions import WorkloadError, ResourceError, ConfigError, DeviceError +from wlauto.exceptions import WorkloadError, ResourceError, DeviceError from wlauto.utils.android import ApkInfo, ANDROID_NORMAL_PERMISSIONS, UNSUPPORTED_PACKAGES from wlauto.utils.types import boolean from wlauto.utils.revent import ReventParser @@ -35,12 +35,12 @@ import wlauto.common.android.resources DELAY = 5 - # Due to the way `super` works you have to call it at every level but WA executes some -# methods conditionally and so has to do them directly via the class, this breaks super +# methods conditionally and so has to call them directly via the class, this breaks super # and causes it to run things mutiple times ect. As a work around for this untill workloads # are reworked everything that subclasses workload calls parent methods explicitly + class UiAutomatorWorkload(Workload): """ Base class for all workloads that rely on a UI Automator JAR file. @@ -173,13 +173,16 @@ class ApkWorkload(Workload): description='Timeout for the installation of the apk.'), Parameter('check_apk', kind=boolean, default=True, description=''' - Discover the APK for this workload on the host, and check that - the version matches the one on device (if already installed). + When set to True the APK file on the host will be prefered if + it is a valid version and ABI, if not it will fall back to the + version on the targer. When set to False the target version is + prefered. '''), Parameter('force_install', kind=boolean, default=False, description=''' Always re-install the APK, even if matching version is found already installed - on the device. Runs ``adb install -r`` to ensure existing APK is replaced. + on the device. Runs ``adb install -r`` to ensure existing APK is replaced. When + this is set, check_apk is ignored. '''), Parameter('uninstall_apk', kind=boolean, default=False, description='If ``True``, will uninstall workload\'s APK as part of teardown.'), @@ -199,88 +202,149 @@ class ApkWorkload(Workload): def setup(self, context): Workload.setup(self, context) - # Get APK for the correct version and device ABI + + # Get target version + target_version = self.device.get_installed_package_version(self.package) + if target_version: + target_version = LooseVersion(target_version) + self.logger.debug("Found version '{}' on target device".format(target_version)) + + # Get host version self.apk_file = context.resolver.get(ApkFile(self, self.device.abi), version=getattr(self, 'version', None), check_abi=getattr(self, 'check_abi', False), variant_name=getattr(self, 'variant_name', None), - strict=self.check_apk) - # Validate the APK - if self.check_apk: - if not self.apk_file: - raise WorkloadError('No APK file found for workload {}.'.format(self.name)) + strict=False) + host_version = None + if self.apk_file is not None: + host_version = ApkInfo(self.apk_file).version_name + if host_version: + host_version = LooseVersion(host_version) + self.logger.debug("Found version '{}' on host".format(host_version)) + + # Error if apk was not found anywhere + if target_version is None and host_version is None: + msg = "Could not find APK for '{}' on the host or target device" + raise ResourceError(msg.format(self.name)) + + # Ensure the apk is setup on the device + if self.force_install: + self.force_install_apk(context, host_version, target_version) + elif self.check_apk: + self.prefer_host_apk(context, host_version, target_version) else: - if self.force_install: - raise ConfigError('force_install cannot be "True" when check_apk is set to "False".') + self.prefer_target_apk(context, host_version, target_version) - self.initialize_package(context) - - # Check the APK version against the min and max versions compatible - # with the workload before launching the package. Note: must be called - # after initialize_package() to get self.apk_version. - if self.check_apk: - self.check_apk_version() + self.reset(context) + self.apk_version = self.device.get_installed_package_version(self.package) + context.add_classifiers(apk_version=self.apk_version) if self.launch_main: - self.launch_package() # launch default activity without intent data + self.launch_package() # launch default activity without intent data self.device.execute('am kill-all') # kill all *background* activities self.device.clear_logcat() - def initialize_package(self, context): - installed_version = self.device.get_installed_package_version(self.package) - if self.check_apk: - self.initialize_with_host_apk(context, installed_version) - else: - if not installed_version: - message = '''{} not found on the device and check_apk is set to "False" - so host version was not checked.''' - raise WorkloadError(message.format(self.package)) - message = 'Version {} installed on device; skipping host APK check.' - self.logger.debug(message.format(installed_version)) - self.reset(context) - self.apk_version = installed_version - context.add_classifiers(apk_version=self.apk_version) + def force_install_apk(self, context, host_version, target_version): + if host_version is None: + raise ResourceError("force_install is 'True' but could not find APK on the host") + try: + self.validate_version(host_version) + except ResourceError as e: + msg = "force_install is 'True' but the host version is invalid:\n\t{}" + raise ResourceError(msg.format(str(e))) + self.install_apk(context, replace=(target_version is not None)) - def initialize_with_host_apk(self, context, installed_version): - host_version = ApkInfo(self.apk_file).version_name - if installed_version != host_version: - if installed_version: - message = '{} host version: {}, device version: {}; re-installing...' - self.logger.debug(message.format(os.path.basename(self.apk_file), - host_version, installed_version)) + def prefer_host_apk(self, context, host_version, target_version): + msg = "check_apk is 'True' " + if host_version is None: + try: + self.validate_version(target_version) + except ResourceError as e: + msg += "but the APK was not found on the host and the target version is invalid:\n\t{}" + raise ResourceError(msg.format(str(e))) else: - message = '{} host version: {}, not found on device; installing...' - self.logger.debug(message.format(os.path.basename(self.apk_file), - host_version)) - self.force_install = True # pylint: disable=attribute-defined-outside-init - else: - message = '{} version {} found on both device and host.' - self.logger.debug(message.format(os.path.basename(self.apk_file), - host_version)) - if self.force_install: - if installed_version: - self.device.uninstall(self.package) - # It's possible that the uninstall above fails, which might result in a warning - # and/or failure during installation. However execution should proceed, so need - # to make sure that the right apk_vesion is reported in the end. - if self.install_apk(context): - self.apk_version = host_version + msg += "but the APK was not found on the host, using target version" + self.logger.debug(msg) + return + try: + self.validate_version(host_version) + except ResourceError as e1: + msg += "but the host APK version is invalid:\n\t{}\n" + if target_version is None: + msg += "The target does not have the app either" + raise ResourceError(msg.format(str(e1))) + try: + self.validate_version(target_version) + except ResourceError as e2: + msg += "The target version is also invalid:\n\t{}" + raise ResourceError(msg.format(str(e1), str(e2))) else: - self.apk_version = installed_version + msg += "using the target version instead" + self.logger.debug(msg.format(str(e1))) + else: # Host version is valid + if target_version is not None and target_version == host_version: + msg += " and a matching version is alread on the device, doing nothing" + self.logger.debug(msg) + return + msg += " and the host version is not on the target, installing APK" + self.logger.debug(msg) + self.install_apk(context, replace=(target_version is not None)) + + def prefer_target_apk(self, context, host_version, target_version): + msg = "check_apk is 'False' " + if target_version is None: + try: + self.validate_version(host_version) + except ResourceError as e: + msg += "but the app was not found on the target and the host version is invalid:\n\t{}" + raise ResourceError(msg.format(str(e))) + else: + msg += "and the app was not found on the target, using host version" + self.logger.debug(msg) + self.install_apk(context) + return + try: + self.validate_version(target_version) + except ResourceError as e1: + msg += "but the target app version is invalid:\n\t{}\n" + if host_version is None: + msg += "The host does not have the APK either" + raise ResourceError(msg.format(str(e1))) + try: + self.validate_version(host_version) + except ResourceError as e2: + msg += "The host version is also invalid:\n\t{}" + raise ResourceError(msg.format(str(e1), str(e2))) + else: + msg += "Using the host APK instead" + self.logger.debug(msg.format(str(e1))) + self.install_apk(context, replace=True) else: - self.apk_version = installed_version - self.reset(context) + msg += "and a valid version of the app is already on the target, using target app" + self.logger.debug(msg) - def check_apk_version(self): - if self.min_apk_version: - if LooseVersion(self.apk_version) < LooseVersion(self.min_apk_version): - message = "APK version not supported. Minimum version required: {}" - raise WorkloadError(message.format(self.min_apk_version)) + def validate_version(self, version): + min_apk_version = getattr(self, 'min_apk_version', None) + max_apk_version = getattr(self, 'max_apk_version', None) - if self.max_apk_version: - if LooseVersion(self.apk_version) > LooseVersion(self.max_apk_version): - message = "APK version not supported. Maximum version supported: {}" - raise WorkloadError(message.format(self.max_apk_version)) + if min_apk_version is not None and max_apk_version is not None: + if version < LooseVersion(min_apk_version) or \ + version > LooseVersion(max_apk_version): + msg = "version '{}' not supported. " \ + "Minimum version required: '{}', Maximum version known to work: '{}'" + raise ResourceError(msg.format(version, min_apk_version)) + + elif min_apk_version is not None: + if version < LooseVersion(min_apk_version): + msg = "version '{}' not supported. " \ + "Minimum version required: '{}'" + raise ResourceError(msg.format(version, min_apk_version)) + + elif max_apk_version is not None: + if version > LooseVersion(max_apk_version): + msg = "version '{}' not supported. " \ + "Maximum version known to work: '{}'" + raise ResourceError(msg.format(version, min_apk_version)) def launch_package(self): if not self.activity: @@ -302,9 +366,9 @@ class ApkWorkload(Workload): if self.device.get_sdk_version() >= 23: self._grant_requested_permissions() - def install_apk(self, context): + def install_apk(self, context, replace=False): success = False - output = self.device.install(self.apk_file, self.install_timeout, replace=self.force_install) + output = self.device.install(self.apk_file, self.install_timeout, replace=replace) if 'Failure' in output: if 'ALREADY_EXISTS' in output: self.logger.warn('Using already installed APK (did not unistall properly?)')