class AndroidUxPerfWorkload(AndroidUiAutoBenchmark):
    __metaclass__ = AndroidUxPerfWorkloadMeta

    deployable_assets = []
    parameters = [
        Parameter('markers_enabled', kind=bool, default=False,
                  description="""
                  If ``True``, UX_PERF action markers will be emitted to logcat during
                  the test run.
                  """),
        Parameter('clean_assets', kind=bool, default=False,
                  description="""
                  If ``True`` pushed assets will be deleted at the end of each iteration
                  """),
        Parameter('force_push_assets', kind=bool, default=False,
                  description="""
                  If ``True`` always push assets on each iteration, even if the
                  assets already exists in the device path
                  """),
    ]

    def _path_on_device(self, fpath, dirname=None):
        if dirname is None:
            dirname = self.device.working_directory
        fname = os.path.basename(fpath)
        return self.device.path.join(dirname, fname)

    def push_assets(self, context):
        for f in self.deployable_assets:
            fpath = context.resolver.get(File(self, f))
            device_path = self._path_on_device(fpath)
            if self.force_push_assets or not self.device.file_exists(device_path):
                self.device.push_file(fpath, device_path, timeout=300)
        self.device.broadcast_media_mounted(self.device.working_directory)

    def delete_assets(self):
        for f in self.deployable_assets:
            self.device.delete_file(self._path_on_device(f))
        self.device.broadcast_media_mounted(self.device.working_directory)

    def __init__(self, device, **kwargs):
        super(AndroidUxPerfWorkload, self).__init__(device, **kwargs)
        # Turn class attribute into instance attribute
        self.deployable_assets = list(self.deployable_assets)

    def validate(self):
        super(AndroidUxPerfWorkload, self).validate()
        self.uiauto_params['package'] = self.package
        self.uiauto_params['markers_enabled'] = self.markers_enabled

    def setup(self, context):
        super(AndroidUxPerfWorkload, self).setup(context)
        self.push_assets(context)

    def teardown(self, context):
        super(AndroidUxPerfWorkload, self).teardown(context)
        if self.clean_assets:
            self.delete_assets()
Exemplo n.º 2
0
class MyOverridingExtension(MyAcidExtension):

    name = 'overriding'

    parameters = [
        Parameter('hydrochloric', override=True, default=[3, 4]),
    ]
Exemplo n.º 3
0
class MultiValueParamExt(Extension):

    name = 'multivalue'

    parameters = [
        Parameter('test', kind=list_of_ints, allowed_values=[42, 7, 73]),
    ]
Exemplo n.º 4
0
class MyModularExtension(Extension):

    name = 'modular'

    parameters = [
        Parameter('modules', override=True, default=['cool_module']),
    ]
Exemplo n.º 5
0
class MyOtherModularExtension(Extension):

    name = 'other_modular'

    parameters = [
        Parameter('modules',
                  override=True,
                  default=[
                      'cool_module',
                      'even_cooler_module',
                  ]),
    ]

    def __init__(self, **kwargs):
        super(MyOtherModularExtension, self).__init__(**kwargs)
        self.self_fizzle_factor = 0
Exemplo n.º 6
0
class MyAcidExtension(MyBaseExtension):

    name = 'acid'

    parameters = [
        Parameter('hydrochloric', kind=list_of_ints, default=[1, 2]),
        'citric',
        ('carbonic', int),
    ]

    def __init__(self, **kwargs):
        super(MyAcidExtension, self).__init__(**kwargs)
        self.vv1 = 0
        self.vv2 = 0

    def virtual1(self):
        self.vv1 += 1
        self.v3 = 'acid'

    def virtual2(self):
        self.vv2 += 1
Exemplo n.º 7
0
class MyBaseExtension(Extension):

    __metaclass__ = MyMeta

    name = 'base'

    parameters = [
        Parameter('base'),
    ]

    def __init__(self, **kwargs):
        super(MyBaseExtension, self).__init__(**kwargs)
        self.v1 = 0
        self.v2 = 0
        self.v3 = ''

    def virtual1(self):
        self.v1 += 1
        self.v3 = 'base'

    def virtual2(self):
        self.v2 += 1
Exemplo n.º 8
0
class BigLittleDevice(AndroidDevice):  # pylint: disable=W0223

    parameters = [
        Parameter('scheduler', default='hmp', override=True),
    ]
Exemplo n.º 9
0
class AndroidDevice(BaseLinuxDevice):  # pylint: disable=W0223
    """
    Device running Android OS.

    """

    platform = 'android'

    parameters = [
        Parameter('adb_name',
                  description=
                  'The unique ID of the device as output by "adb devices".'),
        Parameter(
            'android_prompt',
            kind=regex,
            default=re.compile('^.*(shell|root)@.*:/\S* [#$] ', re.MULTILINE),
            description='The format  of matching the shell prompt in Android.'
        ),
        Parameter(
            'working_directory',
            default='/sdcard/wa-working',
            description=
            'Directory that will be used WA on the device for output files etc.'
        ),
        Parameter('binaries_directory',
                  default='/system/bin',
                  description='Location of binaries on the device.'),
        Parameter(
            'package_data_directory',
            default='/data/data',
            description='Location of of data for an installed package (APK).'),
        Parameter('external_storage_directory',
                  default='/sdcard',
                  description='Mount point for external storage.'),
        Parameter('connection',
                  default='usb',
                  allowed_values=['usb', 'ethernet'],
                  description='Specified the nature of adb connection.'),
        Parameter('logcat_poll_period',
                  kind=int,
                  description="""
                  If specified and is not ``0``, logcat will be polled every
                  ``logcat_poll_period`` seconds, and buffered on the host. This
                  can be used if a lot of output is expected in logcat and the fixed
                  logcat buffer on the device is not big enough. The trade off is that
                  this introduces some minor runtime overhead. Not set by default.
                  """),
        Parameter('enable_screen_check',
                  kind=boolean,
                  default=False,
                  description="""
                  Specified whether the device should make sure that the screen is on
                  during initialization.
                  """),
    ]

    default_timeout = 30
    delay = 2
    long_delay = 3 * delay
    ready_timeout = 60

    # Overwritten from Device. For documentation, see corresponding method in
    # Device.

    @property
    def is_rooted(self):
        if self._is_rooted is None:
            try:
                result = adb_shell(self.adb_name, 'su', timeout=1)
                if 'not found' in result:
                    self._is_rooted = False
                else:
                    self._is_rooted = True
            except TimeoutError:
                self._is_rooted = True
            except DeviceError:
                self._is_rooted = False
        return self._is_rooted

    @property
    def abi(self):
        return self.getprop()['ro.product.cpu.abi'].split('-')[0]

    @property
    def supported_eabi(self):
        props = self.getprop()
        result = [props['ro.product.cpu.abi']]
        if 'ro.product.cpu.abi2' in props:
            result.append(props['ro.product.cpu.abi2'])
        if 'ro.product.cpu.abilist' in props:
            for eabi in props['ro.product.cpu.abilist'].split(','):
                if eabi not in result:
                    result.append(eabi)
        return result

    def __init__(self, **kwargs):
        super(AndroidDevice, self).__init__(**kwargs)
        self._logcat_poller = None

    def reset(self):
        self._is_ready = False
        self._just_rebooted = True
        adb_command(self.adb_name, 'reboot', timeout=self.default_timeout)

    def hard_reset(self):
        super(AndroidDevice, self).hard_reset()
        self._is_ready = False
        self._just_rebooted = True

    def boot(self, **kwargs):
        self.reset()

    def connect(self):  # NOQA pylint: disable=R0912
        iteration_number = 0
        max_iterations = self.ready_timeout / self.delay
        available = False
        self.logger.debug('Polling for device {}...'.format(self.adb_name))
        while iteration_number < max_iterations:
            devices = adb_list_devices()
            if self.adb_name:
                for device in devices:
                    if device.name == self.adb_name and device.status != 'offline':
                        available = True
            else:  # adb_name not set
                if len(devices) == 1:
                    available = True
                elif len(devices) > 1:
                    raise DeviceError(
                        'More than one device is connected and adb_name is not set.'
                    )

            if available:
                break
            else:
                time.sleep(self.delay)
                iteration_number += 1
        else:
            raise DeviceError('Could not boot {} ({}).'.format(
                self.name, self.adb_name))

        while iteration_number < max_iterations:
            available = (1 == int('0' +
                                  adb_shell(self.adb_name,
                                            'getprop sys.boot_completed',
                                            timeout=self.default_timeout)))
            if available:
                break
            else:
                time.sleep(self.delay)
                iteration_number += 1
        else:
            raise DeviceError('Could not boot {} ({}).'.format(
                self.name, self.adb_name))

        if self._just_rebooted:
            self.logger.debug('Waiting for boot to complete...')
            # On some devices, adb connection gets reset some time after booting.
            # This  causes errors during execution. To prevent this, open a shell
            # session and wait for it to be killed. Once its killed, give adb
            # enough time to restart, and then the device should be ready.
            # TODO: This is more of a work-around rather than an actual solution.
            #       Need to figure out what is going on the "proper" way of handling it.
            try:
                adb_shell(self.adb_name, '', timeout=20)
                time.sleep(5)  # give adb time to re-initialize
            except TimeoutError:
                pass  # timed out waiting for the session to be killed -- assume not going to be.

            self.logger.debug('Boot completed.')
            self._just_rebooted = False
        self._is_ready = True

    def initialize(self, context):
        self.execute('mkdir -p {}'.format(self.working_directory))
        if self.is_rooted:
            if not self.executable_is_installed('busybox'):
                self.busybox = self.deploy_busybox(context)
            else:
                self.busybox = self.path.join(self.binaries_directory,
                                              'busybox')
            self.disable_screen_lock()
            self.disable_selinux()
        if self.enable_screen_check:
            self.ensure_screen_is_on()

    def disconnect(self):
        if self._logcat_poller:
            self._logcat_poller.close()

    def ping(self):
        try:
            # May be triggered inside initialize()
            adb_shell(self.adb_name, 'ls /', timeout=10)
        except (TimeoutError, CalledProcessError):
            raise DeviceNotRespondingError(self.adb_name or self.name)

    def start(self):
        if self.logcat_poll_period:
            if self._logcat_poller:
                self._logcat_poller.close()
            self._logcat_poller = _LogcatPoller(self,
                                                self.logcat_poll_period,
                                                timeout=self.default_timeout)
            self._logcat_poller.start()

    def stop(self):
        if self._logcat_poller:
            self._logcat_poller.stop()

    def get_android_version(self):
        return ANDROID_VERSION_MAP.get(self.get_sdk_version(), None)

    def get_android_id(self):
        """
        Get the device's ANDROID_ID. Which is

            "A 64-bit number (as a hex string) that is randomly generated when the user
            first sets up the device and should remain constant for the lifetime of the
            user's device."

        .. note:: This will get reset on userdata erasure.

        """
        return self.execute('settings get secure android_id').strip()

    def get_sdk_version(self):
        try:
            return int(self.getprop('ro.build.version.sdk'))
        except (ValueError, TypeError):
            return None

    def get_installed_package_version(self, package):
        """
        Returns the version (versionName) of the specified package if it is installed
        on the device, or ``None`` otherwise.

        Added in version 2.1.4

        """
        output = self.execute('dumpsys package {}'.format(package))
        for line in convert_new_lines(output).split('\n'):
            if 'versionName' in line:
                return line.split('=', 1)[1]
        return None

    def list_packages(self):
        """
        List packages installed on the device.

        Added in version 2.1.4

        """
        output = self.execute('pm list packages')
        output = output.replace('package:', '')
        return output.split()

    def package_is_installed(self, package_name):
        """
        Returns ``True`` the if a package with the specified name is installed on
        the device, and ``False`` otherwise.

        Added in version 2.1.4

        """
        return package_name in self.list_packages()

    def executable_is_installed(self, executable_name):
        return executable_name in self.listdir(self.binaries_directory)

    def is_installed(self, name):
        return self.executable_is_installed(name) or self.package_is_installed(
            name)

    def listdir(self, path, as_root=False, **kwargs):
        contents = self.execute('ls {}'.format(path), as_root=as_root)
        return [x.strip() for x in contents.split()]

    def push_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221
        """
        Modified in version 2.1.4: added  ``as_root`` parameter.

        """
        self._check_ready()
        try:
            if not as_root:
                adb_command(self.adb_name,
                            "push '{}' '{}'".format(source, dest),
                            timeout=timeout)
            else:
                device_tempfile = self.path.join(self.file_transfer_cache,
                                                 source.lstrip(self.path.sep))
                self.execute('mkdir -p {}'.format(
                    self.path.dirname(device_tempfile)))
                adb_command(self.adb_name,
                            "push '{}' '{}'".format(source, device_tempfile),
                            timeout=timeout)
                self.execute('cp {} {}'.format(device_tempfile, dest),
                             as_root=True)
        except CalledProcessError as e:
            raise DeviceError(e)

    def pull_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221
        """
        Modified in version 2.1.4: added  ``as_root`` parameter.

        """
        self._check_ready()
        try:
            if not as_root:
                adb_command(self.adb_name,
                            "pull '{}' '{}'".format(source, dest),
                            timeout=timeout)
            else:
                device_tempfile = self.path.join(self.file_transfer_cache,
                                                 source.lstrip(self.path.sep))
                self.execute('mkdir -p {}'.format(
                    self.path.dirname(device_tempfile)))
                self.execute('cp {} {}'.format(source, device_tempfile),
                             as_root=True)
                adb_command(self.adb_name,
                            "pull '{}' '{}'".format(device_tempfile, dest),
                            timeout=timeout)
        except CalledProcessError as e:
            raise DeviceError(e)

    def delete_file(self, filepath, as_root=False):  # pylint: disable=W0221
        self._check_ready()
        adb_shell(self.adb_name,
                  "rm '{}'".format(filepath),
                  as_root=as_root,
                  timeout=self.default_timeout)

    def file_exists(self, filepath):
        self._check_ready()
        output = adb_shell(
            self.adb_name,
            'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath),
            timeout=self.default_timeout)
        if int(output):
            return True
        else:
            return False

    def install(self, filepath, timeout=default_timeout, with_name=None):  # pylint: disable=W0221
        ext = os.path.splitext(filepath)[1].lower()
        if ext == '.apk':
            return self.install_apk(filepath, timeout)
        else:
            return self.install_executable(filepath, with_name)

    def install_apk(self, filepath, timeout=default_timeout):  # pylint: disable=W0221
        self._check_ready()
        ext = os.path.splitext(filepath)[1].lower()
        if ext == '.apk':
            return adb_command(self.adb_name,
                               "install {}".format(filepath),
                               timeout=timeout)
        else:
            raise DeviceError(
                'Can\'t install {}: unsupported format.'.format(filepath))

    def install_executable(self, filepath, with_name=None):
        """
        Installs a binary executable on device. Requires root access. Returns
        the path to the installed binary, or ``None`` if the installation has failed.
        Optionally, ``with_name`` parameter may be used to specify a different name under
        which the executable will be installed.

        Added in version 2.1.3.
        Updated in version 2.1.5 with ``with_name`` parameter.

        """
        self._ensure_binaries_directory_is_writable()
        executable_name = with_name or os.path.basename(filepath)
        on_device_file = self.path.join(self.working_directory,
                                        executable_name)
        on_device_executable = self.path.join(self.binaries_directory,
                                              executable_name)
        self.push_file(filepath, on_device_file)
        self.execute('cp {} {}'.format(on_device_file, on_device_executable),
                     as_root=self.is_rooted)
        self.execute('chmod 0777 {}'.format(on_device_executable),
                     as_root=self.is_rooted)
        return on_device_executable

    def uninstall(self, package):
        self._check_ready()
        adb_command(self.adb_name,
                    "uninstall {}".format(package),
                    timeout=self.default_timeout)

    def uninstall_executable(self, executable_name):
        """
        Requires root access.

        Added in version 2.1.3.

        """
        on_device_executable = self.path.join(self.binaries_directory,
                                              executable_name)
        self._ensure_binaries_directory_is_writable()
        self.delete_file(on_device_executable, as_root=self.is_rooted)

    def execute(self,
                command,
                timeout=default_timeout,
                check_exit_code=True,
                background=False,
                as_root=False,
                busybox=False,
                **kwargs):
        """
        Execute the specified command on the device using adb.

        Parameters:

            :param command: The command to be executed. It should appear exactly
                            as if you were typing it into a shell.
            :param timeout: Time, in seconds, to wait for adb to return before aborting
                            and raising an error. Defaults to ``AndroidDevice.default_timeout``.
            :param check_exit_code: If ``True``, the return code of the command on the Device will
                                    be check and exception will be raised if it is not 0.
                                    Defaults to ``True``.
            :param background: If ``True``, will execute adb in a subprocess, and will return
                               immediately, not waiting for adb to return. Defaults to ``False``
            :param busybox: If ``True``, will use busybox to execute the command. Defaults to ``False``.

                            Added in version 2.1.3

                            .. note:: The device must be rooted to be able to use busybox.

            :param as_root: If ``True``, will attempt to execute command in privileged mode. The device
                            must be rooted, otherwise an error will be raised. Defaults to ``False``.

                            Added in version 2.1.3

        :returns: If ``background`` parameter is set to ``True``, the subprocess object will
                  be returned; otherwise, the contents of STDOUT from the device will be returned.

        :raises: DeviceError if adb timed out  or if the command returned non-zero exit
                 code on the device, or if attempting to execute a command in privileged mode on an
                 unrooted device.

        """
        self._check_ready()
        if as_root and not self.is_rooted:
            raise DeviceError(
                'Attempting to execute "{}" as root on unrooted device.'.
                format(command))
        if busybox:
            if not self.is_rooted:
                DeviceError('Attempting to execute "{}" with busybox. '.format(
                    command) +
                            'Busybox can only be deployed to rooted devices.')
            command = ' '.join([self.busybox, command])
        if background:
            return adb_background_shell(self.adb_name,
                                        command,
                                        as_root=as_root)
        else:
            return adb_shell(self.adb_name, command, timeout, check_exit_code,
                             as_root)

    def kick_off(self, command):
        """
        Like execute but closes adb session and returns immediately, leaving the command running on the
        device (this is different from execute(background=True) which keeps adb connection open and returns
        a subprocess object).

        .. note:: This relies on busybox's nohup applet and so won't work on unrooted devices.

        Added in version 2.1.4

        """
        if not self.is_rooted:
            raise DeviceError(
                'kick_off uses busybox\'s nohup applet and so can only be run a rooted device.'
            )
        try:
            command = 'cd {} && busybox nohup {}'.format(
                self.working_directory, command)
            output = self.execute(command, timeout=1, as_root=True)
        except TimeoutError:
            pass
        else:
            raise ValueError(
                'Background command exited before timeout; got "{}"'.format(
                    output))

    def get_pids_of(self, process_name):
        """Returns a list of PIDs of all processes with the specified name."""
        result = self.execute('ps {}'.format(process_name[-15:]),
                              check_exit_code=False).strip()
        if result and 'not found' not in result:
            return [int(x.split()[1]) for x in result.split('\n')[1:]]
        else:
            return []

    def ps(self, **kwargs):
        """
        Returns the list of running processes on the device. Keyword arguments may
        be used to specify simple filters for columns.

        Added in version 2.1.4

        """
        lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
        lines.next()  # header
        result = []
        for line in lines:
            parts = line.split()
            if parts:
                result.append(
                    PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
        if not kwargs:
            return result
        else:
            filtered_result = []
            for entry in result:
                if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
                    filtered_result.append(entry)
            return filtered_result

    def get_properties(self, context):
        """Captures and saves the information from /system/build.prop and /proc/version"""
        props = super(AndroidDevice, self).get_properties(context)
        props['android_id'] = self.get_android_id()
        buildprop_file = os.path.join(context.host_working_directory,
                                      'build.prop')
        if not os.path.isfile(buildprop_file):
            self.pull_file('/system/build.prop',
                           context.host_working_directory)
        self._update_build_properties(buildprop_file, props)
        context.add_run_artifact('build_properties', buildprop_file, 'export')

        dumpsys_window_file = os.path.join(context.host_working_directory,
                                           'window.dumpsys')
        dumpsys_window_output = self.execute('dumpsys window')
        with open(dumpsys_window_file, 'w') as wfh:
            wfh.write(dumpsys_window_output)
        context.add_run_artifact('dumpsys_window', dumpsys_window_file, 'meta')
        return props

    def getprop(self, prop=None):
        """Returns parsed output of Android getprop command. If a property is
        specified, only the value for that property will be returned (with
        ``None`` returned if the property doesn't exist. Otherwise,
        ``wlauto.utils.android.AndroidProperties`` will be returned, which is
        a dict-like object."""
        props = AndroidProperties(self.execute('getprop'))
        if prop:
            return props[prop]
        return props

    # Android-specific methods. These either rely on specifics of adb or other
    # Android-only concepts in their interface and/or implementation.

    def forward_port(self, from_port, to_port):
        """
        Forward a port on the device to a port on localhost.

        :param from_port: Port on the device which to forward.
        :param to_port: Port on the localhost to which the device port will be forwarded.

        Ports should be specified using adb spec. See the "adb forward" section in "adb help".

        """
        adb_command(self.adb_name,
                    'forward {} {}'.format(from_port, to_port),
                    timeout=self.default_timeout)

    def dump_logcat(self, outfile, filter_spec=None):
        """
        Dump the contents of logcat, for the specified filter spec to the
        specified output file.
        See http://developer.android.com/tools/help/logcat.html

        :param outfile: Output file on the host into which the contents of the
                        log will be written.
        :param filter_spec: Logcat filter specification.
                            see http://developer.android.com/tools/debugging/debugging-log.html#filteringOutput

        """
        if self._logcat_poller:
            return self._logcat_poller.write_log(outfile)
        else:
            if filter_spec:
                command = 'logcat -d -s {} > {}'.format(filter_spec, outfile)
            else:
                command = 'logcat -d > {}'.format(outfile)
            return adb_command(self.adb_name,
                               command,
                               timeout=self.default_timeout)

    def clear_logcat(self):
        """Clear (flush) logcat log."""
        if self._logcat_poller:
            return self._logcat_poller.clear_buffer()
        else:
            return adb_shell(self.adb_name,
                             'logcat -c',
                             timeout=self.default_timeout)

    def capture_screen(self, filepath):
        """Caputers the current device screen into the specified file in a PNG format."""
        on_device_file = self.path.join(self.working_directory,
                                        'screen_capture.png')
        self.execute('screencap -p  {}'.format(on_device_file))
        self.pull_file(on_device_file, filepath)
        self.delete_file(on_device_file)

    def is_screen_on(self):
        """Returns ``True`` if the device screen is currently on, ``False`` otherwise."""
        output = self.execute('dumpsys power')
        match = SCREEN_STATE_REGEX.search(output)
        if match:
            return boolean(match.group(1))
        else:
            raise DeviceError('Could not establish screen state.')

    def ensure_screen_is_on(self):
        if not self.is_screen_on():
            self.execute('input keyevent 26')

    def disable_screen_lock(self):
        """
        Attempts to disable he screen lock on the device.

        .. note:: This does not always work...

        Added inversion 2.1.4

        """
        lockdb = '/data/system/locksettings.db'
        sqlcommand = "update locksettings set value=\\'0\\' where name=\\'screenlock.disabled\\';"
        self.execute('sqlite3 {} "{}"'.format(lockdb, sqlcommand),
                     as_root=True)

    def disable_selinux(self):
        # This may be invoked from intialize() so we can't use execute() or the
        # standard API for doing this.
        api_level = int(
            adb_shell(self.adb_name,
                      'getprop ro.build.version.sdk',
                      timeout=self.default_timeout).strip())
        # SELinux was added in Android 4.3 (API level 18). Trying to
        # 'getenforce' in earlier versions will produce an error.
        if api_level >= 18:
            se_status = self.execute('getenforce', as_root=True).strip()
            if se_status == 'Enforcing':
                self.execute('setenforce 0', as_root=True)

    # Internal methods: do not use outside of the class.

    def _update_build_properties(self, filepath, props):
        try:
            with open(filepath) as fh:
                for line in fh:
                    line = re.sub(r'#.*', '', line).strip()
                    if not line:
                        continue
                    key, value = line.split('=', 1)
                    props[key] = value
        except ValueError:
            self.logger.warning('Could not parse build.prop.')

    def _update_versions(self, filepath, props):
        with open(filepath) as fh:
            text = fh.read()
            props['version'] = text
            text = re.sub(r'#.*', '', text).strip()
            match = re.search(r'^(Linux version .*?)\s*\((gcc version .*)\)$',
                              text)
            if match:
                props['linux_version'] = match.group(1).strip()
                props['gcc_version'] = match.group(2).strip()
            else:
                self.logger.warning('Could not parse version string.')

    def _ensure_binaries_directory_is_writable(self):
        matched = []
        for entry in self.list_file_systems():
            if self.binaries_directory.rstrip('/').startswith(
                    entry.mount_point):
                matched.append(entry)
        if matched:
            entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
            if 'rw' not in entry.options:
                self.execute('mount -o rw,remount {} {}'.format(
                    entry.device, entry.mount_point),
                             as_root=True)
        else:
            raise DeviceError(
                'Could not find mount point for binaries directory {}'.format(
                    self.binaries_directory))
Exemplo n.º 10
0
class ApkWorkload(Workload):
    """
    A workload based on an APK file.

    Defines the following attributes:

    :package: The package name of the app. This is usually a Java-style name of the form
              ``com.companyname.appname``.
    :activity: This is the initial activity of the app. This will be used to launch the
               app during the setup.  Many applications do not specify a launch activity so
               this may be left blank if necessary.
    :view: The class of the main view pane of the app. This needs to be defined in order
           to collect SurfaceFlinger-derived statistics (such as FPS) for the app, but
           may otherwise be left as ``None``.
    :install_timeout: Timeout for the installation of the APK. This may vary wildly based on
                      the size and nature of a specific APK, and so should be defined on
                      per-workload basis.

                      .. note:: To a lesser extent, this will also vary based on the the
                                device and the nature of adb connection (USB vs Ethernet),
                                so, as with all timeouts, so leeway must be included in
                                the specified value.

    .. note:: Both package and activity for a workload may be obtained from the APK using
              the ``aapt`` tool that comes with the ADT  (Android Developemnt Tools) bundle.

    """
    package = None
    activity = None
    view = None
    supported_platforms = ['android']

    parameters = [
        Parameter('install_timeout',
                  kind=int,
                  default=300,
                  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).
                  '''),
        Parameter('force_install',
                  kind=boolean,
                  default=False,
                  description='''
                  Always re-install the APK, even if matching version is found
                  on already installed on the device.
                  '''),
        Parameter(
            'uninstall_apk',
            kind=boolean,
            default=False,
            description=
            'If ``True``, will uninstall workload\'s APK as part of teardown.'
        ),
    ]

    def __init__(self, device, _call_super=True, **kwargs):
        if _call_super:
            super(ApkWorkload, self).__init__(device, **kwargs)
        self.apk_file = None
        self.apk_version = None
        self.logcat_log = None

    def init_resources(self, context):
        self.apk_file = context.resolver.get(
            wlauto.common.android.resources.ApkFile(self),
            version=getattr(self, 'version', None),
            strict=self.check_apk)

    def validate(self):
        if self.check_apk:
            if not self.apk_file:
                raise WorkloadError(
                    'No APK file found for workload {}.'.format(self.name))
        else:
            if self.force_install:
                raise ConfigError(
                    'force_install cannot be "True" when check_apk is set to "False".'
                )

    def setup(self, context):
        self.initialize_package(context)
        self.launch_package()
        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 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 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))
            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)
            self.install_apk(context)
        else:
            self.reset(context)
        self.apk_version = host_version

    def launch_package(self):
        if not self.activity:
            output = self.device.execute('am start -W {}'.format(self.package))
        else:
            output = self.device.execute('am start -W -n {}/{}'.format(
                self.package, self.activity))
        if 'Error:' in output:
            self.device.execute('am force-stop {}'.format(
                self.package))  # this will dismiss any erro dialogs
            raise WorkloadError(output)
        self.logger.debug(output)

    def reset(self, context):  # pylint: disable=W0613
        self.device.execute('am force-stop {}'.format(self.package))
        self.device.execute('pm clear {}'.format(self.package))

        # As of android API level 23, apps can request permissions at runtime,
        # this will grant all of them so requests do not pop up when running the app
        if self.device.get_sdk_version() >= 23:
            self._grant_requested_permissions()

    def install_apk(self, context):
        output = self.device.install(self.apk_file, self.install_timeout)
        if 'Failure' in output:
            if 'ALREADY_EXISTS' in output:
                self.logger.warn(
                    'Using already installed APK (did not unistall properly?)')
            else:
                raise WorkloadError(output)
        else:
            self.logger.debug(output)
        self.do_post_install(context)

    def _grant_requested_permissions(self):
        dumpsys_output = self.device.execute(
            command="dumpsys package {}".format(self.package))
        permissions = []
        lines = iter(dumpsys_output.splitlines())
        for line in lines:
            if "requested permissions:" in line:
                break

        for line in lines:
            if "android.permission." in line:
                permissions.append(line.split(":")[0].strip())
            else:
                break

        for permission in permissions:
            # "Normal" Permisions are automatically granted and cannot be changed
            permission_name = permission.rsplit('.', 1)[1]
            if permission_name not in ANDROID_NORMAL_PERMISSIONS:
                self.device.execute("pm grant {} {}".format(
                    self.package, permission))

    def do_post_install(self, context):
        """ May be overwritten by dervied classes."""
        pass

    def run(self, context):
        pass

    def update_result(self, context):
        self.logcat_log = os.path.join(context.output_directory, 'logcat.log')
        self.device.dump_logcat(self.logcat_log)
        context.add_iteration_artifact(name='logcat',
                                       path='logcat.log',
                                       kind='log',
                                       description='Logact dump for the run.')

    def teardown(self, context):
        self.device.execute('am force-stop {}'.format(self.package))
        if self.uninstall_apk:
            self.device.uninstall(self.package)
Exemplo n.º 11
0
class ReventWorkload(Workload):
    # pylint: disable=attribute-defined-outside-init

    description = """
    A workload for playing back revent recordings. You can supply three
    different files:
    
    1. {device_model}.setup.revent
    2. {device_model}.run.revent
    3. {device_model}.teardown.revent

    You may generate these files using the wa record command using the -s flag
    to specify the stage (``setup``, ``run``, ``teardown``)

    You may also supply an 'idle_time' in seconds in place of the run file.
    The ``run`` file may only be omitted if you choose to run this way, but
    while running idle may supply ``setup`` and ``teardown`` files.

    To use a ``setup`` or ``teardown`` file set the setup_required and/or
    teardown_required class attributes to True (default: False).

    N.B. This is the default description. You may overwrite this for your
    workload to include more specific information.

    """

    setup_required = False
    teardown_required = False

    parameters = [
        Parameter(
            'idle_time', kind=int, default=None,
             description='''
             The time you wish the device to remain idle for (if a value is
             given then this overrides any .run revent file).
             '''),
    ]

    def __init__(self, device, _call_super=True, **kwargs):
        if _call_super:
            Workload.__init__(self, device, **kwargs)
        self.setup_timeout = kwargs.get('setup_timeout', None)
        self.run_timeout = kwargs.get('run_timeout', None)
        self.teardown_timeout = kwargs.get('teardown_timeout', None)
        self.revent_setup_file = None
        self.revent_run_file = None
        self.revent_teardown_file = None
        self.on_device_setup_revent = None
        self.on_device_run_revent = None
        self.on_device_teardown_revent = None
        self.statedefs_dir = None

    def initialize(self, context):
        devpath = self.device.path
        self.on_device_revent_binary = devpath.join(self.device.binaries_directory, 'revent')

    def setup(self, context):
        devpath = self.device.path
        if self.setup_required:
            self.revent_setup_file = context.resolver.get(ReventFile(self, 'setup'))
            if self.revent_setup_file:
                self.on_device_setup_revent = devpath.join(self.device.working_directory,
                                                 os.path.split(self.revent_setup_file)[-1])
                duration = ReventRecording(self.revent_setup_file).duration
                self.default_setup_timeout = ceil(duration) + 30
        if not self.idle_time:
            self.revent_run_file = context.resolver.get(ReventFile(self, 'run'))
            if self.revent_run_file:
                self.on_device_run_revent = devpath.join(self.device.working_directory,
                                                 os.path.split(self.revent_run_file)[-1])
                self.default_run_timeout = ceil(ReventRecording(self.revent_run_file).duration) + 30
        if self.teardown_required:
            self.revent_teardown_file = context.resolver.get(ReventFile(self, 'teardown'))
            if self.revent_teardown_file:
                self.on_device_teardown_revent = devpath.join(self.device.working_directory,
                                                 os.path.split(self.revent_teardown_file)[-1])
                duration = ReventRecording(self.revent_teardown_file).duration
                self.default_teardown_timeout = ceil(duration) + 30
        self._check_revent_files(context)

        Workload.setup(self, context)

        if self.revent_setup_file is not None:
            self.setup_timeout = self.setup_timeout or self.default_setup_timeout
            self.device.killall('revent')
            command = '{} replay {}'.format(self.on_device_revent_binary, self.on_device_setup_revent)
            self.device.execute(command, timeout=self.setup_timeout)
            self.logger.debug('Revent setup completed.')

    def run(self, context):
        if not self.idle_time:
            self.run_timeout = self.run_timeout or self.default_run_timeout
            command = '{} replay {}'.format(self.on_device_revent_binary, self.on_device_run_revent)
            self.logger.debug('Replaying {}'.format(os.path.basename(self.on_device_run_revent)))
            self.device.execute(command, timeout=self.run_timeout)
            self.logger.debug('Replay completed.')
        else:
            self.logger.info('Idling for ' + str(self.idle_time) + ' seconds.')
            self.device.sleep(self.idle_time)
            self.logger.info('Successfully did nothing for ' + str(self.idle_time) + ' seconds!')

    def update_result(self, context):
        pass

    def teardown(self, context):
        if self.revent_teardown_file is not None:
            self.teardown_timeout = self.teardown_timeout or self.default_teardown_timeout
            command = '{} replay {}'.format(self.on_device_revent_binary,
                                            self.on_device_teardown_revent)
            self.device.execute(command, timeout=self.teardown_timeout)
            self.logger.debug('Replay completed.')
            self.device.killall('revent')
        if self.revent_setup_file is not None:
            self.device.delete_file(self.on_device_setup_revent)
        if not self.idle_time:
            self.device.delete_file(self.on_device_run_revent)
        if self.revent_teardown_file is not None:
            self.device.delete_file(self.on_device_teardown_revent)

    def _check_revent_files(self, context):
        # check the revent binary
        revent_binary = context.resolver.get(Executable(NO_ONE, self.device.abi, 'revent'))
        if not os.path.isfile(revent_binary):
            message = '{} does not exist. '.format(revent_binary)
            message += 'Please build revent for your system and place it in that location'
            raise WorkloadError(message)
        if not self.revent_run_file and not self.idle_time:
            # pylint: disable=too-few-format-args
            message = 'It seems a {0}.run.revent file does not exist, '\
                      'Please provide one for your device: {0}.'
            raise WorkloadError(message.format(self.device.name))

        self.on_device_revent_binary = self.device.install_executable(revent_binary)
        if self.revent_setup_file is not None:
            self.device.push_file(self.revent_setup_file, self.on_device_setup_revent)
        if not self.idle_time:
            self.device.push_file(self.revent_run_file, self.on_device_run_revent)
        if self.revent_teardown_file is not None:
            self.device.push_file(self.revent_teardown_file, self.on_device_teardown_revent)
Exemplo n.º 12
0
class LinuxDevice(BaseLinuxDevice):

    platform = 'linux'

    default_timeout = 30
    delay = 2
    long_delay = 3 * delay
    ready_timeout = 60

    parameters = [
        Parameter('host', mandatory=True, description='Host name or IP address for the device.'),
        Parameter('username', mandatory=True, description='User name for the account on the device.'),
        Parameter('password', description='Password for the account on the device (for password-based auth).'),
        Parameter('keyfile', description='Keyfile to be used for key-based authentication.'),
        Parameter('port', kind=int, default=22, description='SSH port number on the device.'),
        Parameter('password_prompt', default='[sudo] password',
                  description='Prompt presented by sudo when requesting the password.'),

        Parameter('use_telnet', kind=boolean, default=False,
                  description='Optionally, telnet may be used instead of ssh, though this is discouraged.'),
        Parameter('boot_timeout', kind=int, default=120,
                  description='How long to try to connect to the device after a reboot.'),

        Parameter('working_directory', default=None,
                  description='''
                  Working directory to be used by WA. This must be in a location where the specified user
                  has write permissions. This will default to /home/<username>/wa (or to /root/wa, if
                  username is 'root').
                  '''),
        Parameter('binaries_directory', default='/usr/local/bin',
                  description='Location of executable binaries on this device (must be in PATH).'),
    ]

    @property
    def is_rooted(self):
        if self._is_rooted is None:
            # First check if the user is root
            try:
                self.execute('test $(id -u) = 0')
                self._is_root_user = True
                self._is_rooted = True
                return self._is_rooted
            except DeviceError:
                self._is_root_user = False

            # Otherwise, check if the user has sudo rights
            try:
                self.execute('ls /', as_root=True)
                self._is_rooted = True
            except DeviceError:
                self._is_rooted = False
        return self._is_rooted

    def __init__(self, *args, **kwargs):
        super(LinuxDevice, self).__init__(*args, **kwargs)
        self.shell = None
        self.local_binaries_directory = None
        self._is_rooted = None

    def validate(self):
        if self.working_directory is None:  # pylint: disable=access-member-before-definition
            if self.username == 'root':
                self.working_directory = '/root/wa'  # pylint: disable=attribute-defined-outside-init
            else:
                self.working_directory = '/home/{}/wa'.format(self.username)  # pylint: disable=attribute-defined-outside-init
        self.local_binaries_directory = self.path.join(self.working_directory, 'bin')

    def initialize(self, context, *args, **kwargs):
        self.execute('mkdir -p {}'.format(self.local_binaries_directory))
        self.execute('mkdir -p {}'.format(self.binaries_directory))
        self.execute('export PATH={}:$PATH'.format(self.local_binaries_directory))
        self.execute('export PATH={}:$PATH'.format(self.binaries_directory))
        super(LinuxDevice, self).initialize(context, *args, **kwargs)

    # Power control

    def reset(self):
        self.execute('reboot', as_root=True)
        self._is_ready = False

    def hard_reset(self):
        super(LinuxDevice, self).hard_reset()
        self._is_ready = False

    def boot(self, hard=False, **kwargs):
        if hard:
            self.hard_reset()
        else:
            self.reset()
        self.logger.debug('Waiting for device...')
        start_time = time.time()
        while (time.time() - start_time) < self.boot_timeout:
            try:
                s = socket.create_connection((self.host, self.port), timeout=5)
                s.close()
                break
            except socket.timeout:
                pass
            except socket.error:
                time.sleep(5)
        else:
            raise DeviceError('Could not connect to {} after reboot'.format(self.host))

    def connect(self):  # NOQA pylint: disable=R0912
        self.shell = SshShell(password_prompt=self.password_prompt,
                              timeout=self.default_timeout, telnet=self.use_telnet)
        self.shell.login(self.host, self.username, self.password, self.keyfile, self.port)
        self._is_ready = True

    def disconnect(self):  # NOQA pylint: disable=R0912
        self.shell.logout()
        self._is_ready = False

    # Execution

    def has_root(self):
        try:
            self.execute('ls /', as_root=True)
            return True
        except DeviceError as e:
            if 'not in the sudoers file' not in e.message:
                raise e
            return False

    def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False,
                as_root=False, strip_colors=True, **kwargs):
        """
        Execute the specified command on the device using adb.

        Parameters:

            :param command: The command to be executed. It should appear exactly
                            as if you were typing it into a shell.
            :param timeout: Time, in seconds, to wait for adb to return before aborting
                            and raising an error. Defaults to ``AndroidDevice.default_timeout``.
            :param check_exit_code: If ``True``, the return code of the command on the Device will
                                    be check and exception will be raised if it is not 0.
                                    Defaults to ``True``.
            :param background: If ``True``, will execute create a new ssh shell rather than using
                               the default session and will return it immediately. If this is ``True``,
                               ``timeout``, ``strip_colors`` and (obvisously) ``check_exit_code`` will
                               be ignored; also, with this, ``as_root=True``  is only valid if ``username``
                               for the device was set to ``root``.
            :param as_root: If ``True``, will attempt to execute command in privileged mode. The device
                            must be rooted, otherwise an error will be raised. Defaults to ``False``.

                            Added in version 2.1.3

        :returns: If ``background`` parameter is set to ``True``, the subprocess object will
                  be returned; otherwise, the contents of STDOUT from the device will be returned.

        """
        self._check_ready()
        try:
            if background:
                if as_root and self.username != 'root':
                    raise DeviceError('Cannot execute in background with as_root=True unless user is root.')
                return self.shell.background(command)
            else:
                # If we're already the root user, don't bother with sudo
                if self._is_root_user:
                    as_root = False
                return self.shell.execute(command, timeout, check_exit_code, as_root, strip_colors)
        except CalledProcessError as e:
            raise DeviceError(e)

    def kick_off(self, command):
        """
        Like execute but closes adb session and returns immediately, leaving the command running on the
        device (this is different from execute(background=True) which keeps adb connection open and returns
        a subprocess object).

        """
        self._check_ready()
        command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
        return self.shell.execute(command)

    def get_pids_of(self, process_name):
        """Returns a list of PIDs of all processes with the specified name."""
        # result should be a column of PIDs with the first row as "PID" header
        result = self.execute('ps -C {} -o pid'.format(process_name),  # NOQA
                              check_exit_code=False).strip().split()
        if len(result) >= 2:  # at least one row besides the header
            return map(int, result[1:])
        else:
            return []

    def ps(self, **kwargs):
        command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
        lines = iter(convert_new_lines(self.execute(command)).split('\n'))
        lines.next()  # header

        result = []
        for line in lines:
            parts = re.split(r'\s+', line, maxsplit=8)
            if parts:
                result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))

        if not kwargs:
            return result
        else:
            filtered_result = []
            for entry in result:
                if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
                    filtered_result.append(entry)
            return filtered_result

    # File management

    def push_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221
        self._check_ready()
        try:
            if not as_root or self.username == 'root':
                self.shell.push_file(source, dest, timeout=timeout)
            else:
                tempfile = self.path.join(self.working_directory, self.path.basename(dest))
                self.shell.push_file(source, tempfile, timeout=timeout)
                self.shell.execute('cp -r {} {}'.format(tempfile, dest), timeout=timeout, as_root=True)
        except CalledProcessError as e:
            raise DeviceError(e)

    def pull_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221
        self._check_ready()
        try:
            if not as_root or self.username == 'root':
                self.shell.pull_file(source, dest, timeout=timeout)
            else:
                tempfile = self.path.join(self.working_directory, self.path.basename(source))
                self.shell.execute('cp -r {} {}'.format(source, tempfile), timeout=timeout, as_root=True)
                self.shell.execute('chown -R {} {}'.format(self.username, tempfile), timeout=timeout, as_root=True)
                self.shell.pull_file(tempfile, dest, timeout=timeout)
        except CalledProcessError as e:
            raise DeviceError(e)

    def delete_file(self, filepath, as_root=False):  # pylint: disable=W0221
        self.execute('rm -rf {}'.format(filepath), as_root=as_root)

    def file_exists(self, filepath):
        output = self.execute('if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
        # output from ssh my contain part of the expression in the buffer,
        # split out everything except the last word.
        return boolean(output.split()[-1])  # pylint: disable=maybe-no-member

    def listdir(self, path, as_root=False, **kwargs):
        contents = self.execute('ls -1 {}'.format(path), as_root=as_root).strip()
        if not contents:
            return []
        return [x.strip() for x in contents.split('\n')]  # pylint: disable=maybe-no-member

    def install(self, filepath, timeout=default_timeout, with_name=None):  # pylint: disable=W0221
        if self.is_rooted:
            destpath = self.path.join(self.binaries_directory,
                                      with_name and with_name or self.path.basename(filepath))
            self.push_file(filepath, destpath, as_root=True)
            self.execute('chmod a+x {}'.format(destpath), timeout=timeout, as_root=True)
        else:
            destpath = self.path.join(self.local_binaries_directory,
                                      with_name and with_name or self.path.basename(filepath))
            self.push_file(filepath, destpath)
            self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
        return destpath

    install_executable = install  # compatibility

    def uninstall(self, name):
        if self.is_rooted:
            path = self.path.join(self.binaries_directory, name)
            self.delete_file(path, as_root=True)
        else:
            path = self.path.join(self.local_binaries_directory, name)
            self.delete_file(path)

    uninstall_executable = uninstall  # compatibility

    def is_installed(self, name):
        try:
            self.execute('which {}'.format(name))
            return True
        except DeviceError:
            return False

    # misc

    def ping(self):
        try:
            # May be triggered inside initialize()
            self.shell.execute('ls /', timeout=5)
        except (TimeoutError, CalledProcessError):
            raise DeviceNotRespondingError(self.host)

    def capture_screen(self, filepath):
        if not self.is_installed('scrot'):
            self.logger.debug('Could not take screenshot as scrot is not installed.')
            return
        try:
            tempfile = self.path.join(self.working_directory, os.path.basename(filepath))
            self.execute('DISPLAY=:0.0 scrot {}'.format(tempfile))
            self.pull_file(tempfile, filepath)
            self.delete_file(tempfile)
        except DeviceError as e:
            if "Can't open X dispay." not in e.message:
                raise e
            message = e.message.split('OUTPUT:', 1)[1].strip()
            self.logger.debug('Could not take screenshot: {}'.format(message))

    def is_screen_on(self):
        pass  # TODO

    def ensure_screen_is_on(self):
        pass  # TODO
Exemplo n.º 13
0
class BaseLinuxDevice(Device):  # pylint: disable=abstract-method

    path_module = 'posixpath'
    has_gpu = True

    parameters = [
        Parameter('scheduler', kind=str, default='unknown',
                  allowed_values=['unknown', 'smp', 'hmp', 'iks', 'ea', 'other'],
                  description="""
                  Specifies the type of multi-core scheduling model utilized in the device. The value
                  must be one of the following:

                  :unknown: A generic Device interface is used to interact with the underlying device
                            and the underlying scheduling model is unkown.
                  :smp: A standard single-core or Symmetric Multi-Processing system.
                  :hmp: ARM Heterogeneous Multi-Processing system.
                  :iks: Linaro In-Kernel Switcher.
                  :ea: ARM Energy-Aware scheduler.
                  :other: Any other system not covered by the above.

                          .. note:: most currently-available systems would fall under ``smp`` rather than
                                    this value. ``other`` is there to future-proof against new schemes
                                    not yet covered by WA.

                  """),
        Parameter('iks_switch_frequency', kind=int, default=None,
                  description="""
                 This is the switching frequency, in kilohertz, of IKS devices. This parameter *MUST NOT*
                 be set for non-IKS device (i.e. ``scheduler != 'iks'``). If left unset for IKS devices,
                 it will default to ``800000``, i.e. 800MHz.
                 """),
        Parameter('property_files', kind=list_of_strings,
                  default=[
                      '/etc/arch-release',
                      '/etc/debian_version',
                      '/etc/lsb-release',
                      '/proc/config.gz',
                      '/proc/cmdline',
                      '/proc/cpuinfo',
                      '/proc/version',
                      '/proc/zconfig',
                      '/sys/kernel/debug/sched_features',
                      '/sys/kernel/hmp',
                  ],
                  description='''
                  A list of paths to files containing static OS properties. These will be pulled into the
                  __meta directory in output for each run in order to provide information about the platfrom.
                  These paths do not have to exist and will be ignored if the path is not present on a
                  particular device.
                  '''),

    ]

    runtime_parameters = [
        RuntimeParameter('sysfile_values', 'get_sysfile_values', 'set_sysfile_values', value_name='params'),
        CoreParameter('${core}_cores', 'get_number_of_online_cpus', 'set_number_of_online_cpus',
                      value_name='number'),
        CoreParameter('${core}_min_frequency', 'get_core_min_frequency', 'set_core_min_frequency',
                      value_name='freq'),
        CoreParameter('${core}_max_frequency', 'get_core_max_frequency', 'set_core_max_frequency',
                      value_name='freq'),
        CoreParameter('${core}_frequency', 'get_core_cur_frequency', 'set_core_cur_frequency',
                      value_name='freq'),
        CoreParameter('${core}_governor', 'get_core_governor', 'set_core_governor',
                      value_name='governor'),
        CoreParameter('${core}_governor_tunables', 'get_core_governor_tunables', 'set_core_governor_tunables',
                      value_name='tunables'),
    ]

    dynamic_modules = [
        'devcpufreq',
        'cpuidle',
    ]

    @property
    def abi(self):
        if not self._abi:
            val = self.execute('uname -m').strip()
            for abi, architectures in ABI_MAP.iteritems():
                if val in architectures:
                    self._abi = abi
                    break
            else:
                self._abi = val
        return self._abi

    @property
    def online_cpus(self):
        val = self.get_sysfile_value('/sys/devices/system/cpu/online')
        return ranges_to_list(val)

    @property
    def number_of_cores(self):
        """
        Added in version 2.1.4.

        """
        if self._number_of_cores is None:
            corere = re.compile(r'^\s*cpu\d+\s*$')
            output = self.execute('ls /sys/devices/system/cpu')
            self._number_of_cores = 0
            for entry in output.split():
                if corere.match(entry):
                    self._number_of_cores += 1
        return self._number_of_cores

    @property
    def resource_cache(self):
        return self.path.join(self.working_directory, '.cache')

    @property
    def file_transfer_cache(self):
        return self.path.join(self.working_directory, '.transfer')

    @property
    def cpuinfo(self):
        if not self._cpuinfo:
            self._cpuinfo = Cpuinfo(self.execute('cat /proc/cpuinfo'))
        return self._cpuinfo

    def __init__(self, **kwargs):
        super(BaseLinuxDevice, self).__init__(**kwargs)
        self.busybox = None
        self._is_initialized = False
        self._is_ready = False
        self._just_rebooted = False
        self._is_rooted = None
        self._is_root_user = False
        self._available_frequencies = {}
        self._available_governors = {}
        self._available_governor_tunables = {}
        self._number_of_cores = None
        self._written_sysfiles = []
        self._cpuinfo = None
        self._abi = None

    def validate(self):
        if self.iks_switch_frequency is not None and self.scheduler != 'iks':  # pylint: disable=E0203
            raise ConfigError('iks_switch_frequency must NOT be set for non-IKS devices.')
        if self.iks_switch_frequency is None and self.scheduler == 'iks':  # pylint: disable=E0203
            self.iks_switch_frequency = 800000  # pylint: disable=W0201

    def initialize(self, context):
        self.execute('mkdir -p {}'.format(self.working_directory))
        if self.is_rooted:
            if not self.is_installed('busybox'):
                self.busybox = self.deploy_busybox(context)
            else:
                self.busybox = 'busybox'

    def is_file(self, filepath):
        output = self.execute('if [ -f \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
        # output from ssh my contain part of the expression in the buffer,
        # split out everything except the last word.
        return boolean(output.split()[-1])  # pylint: disable=maybe-no-member

    def is_directory(self, filepath):
        output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
        # output from ssh my contain part of the expression in the buffer,
        # split out everything except the last word.
        return boolean(output.split()[-1])  # pylint: disable=maybe-no-member

    def get_properties(self, context):
        for propfile in self.property_files:
            try:
                normname = propfile.lstrip(self.path.sep).replace(self.path.sep, '.')
                outfile = os.path.join(context.host_working_directory, normname)
                if self.is_file(propfile):
                    with open(outfile, 'w') as wfh:
                        wfh.write(self.execute('cat {}'.format(propfile)))
                elif self.is_directory(propfile):
                    self.pull_file(propfile, outfile)
                else:
                    continue
            except DeviceError:
                # We pull these files "opportunistically", so if a pull fails
                # (e.g. we don't have permissions to read the file), just note
                # it quietly (not as an error/warning) and move on.
                self.logger.debug('Could not pull property file "{}"'.format(propfile))
        return {}

    def get_sysfile_value(self, sysfile, kind=None):
        """
        Get the contents of the specified sysfile.

        :param sysfile: The file who's contents will be returned.

        :param kind: The type of value to be expected in the sysfile. This can
                     be any Python callable that takes a single str argument.
                     If not specified or is None, the contents will be returned
                     as a string.

        """
        output = self.execute('cat \'{}\''.format(sysfile), as_root=self.is_rooted).strip()  # pylint: disable=E1103
        if kind:
            return kind(output)
        else:
            return output

    def set_sysfile_value(self, sysfile, value, verify=True):
        """
        Set the value of the specified sysfile. By default, the value will be checked afterwards.
        Can be overridden by setting ``verify`` parameter to ``False``.

        """
        value = str(value)
        self.execute('echo {} > \'{}\''.format(value, sysfile), check_exit_code=False, as_root=True)
        if verify:
            output = self.get_sysfile_value(sysfile)
            if not output.strip() == value:  # pylint: disable=E1103
                message = 'Could not set the value of {} to {}'.format(sysfile, value)
                raise DeviceError(message)
        self._written_sysfiles.append(sysfile)

    def get_sysfile_values(self):
        """
        Returns a dict mapping paths of sysfiles that were previously set to their
        current values.

        """
        values = {}
        for sysfile in self._written_sysfiles:
            values[sysfile] = self.get_sysfile_value(sysfile)
        return values

    def set_sysfile_values(self, params):
        """
        The plural version of ``set_sysfile_value``. Takes a single parameter which is a mapping of
        file paths to values to be set. By default, every value written will be verified. The can
        be disabled for individual paths by appending ``'!'`` to them.

        """
        for sysfile, value in params.iteritems():
            verify = not sysfile.endswith('!')
            sysfile = sysfile.rstrip('!')
            self.set_sysfile_value(sysfile, value, verify=verify)

    def deploy_busybox(self, context, force=False):
        """
        Deploys the busybox binary to the specified device and returns
        the path to the binary on the device.

        :param device: device to deploy the binary to.
        :param context: an instance of ExecutionContext
        :param force: by default, if the binary is already present on the
                    device, it will not be deployed again. Setting force
                    to ``True`` overrides that behavior and ensures that the
                    binary is always copied. Defaults to ``False``.

        :returns: The on-device path to the busybox binary.

        """
        on_device_executable = self.path.join(self.binaries_directory, 'busybox')
        if not force and self.file_exists(on_device_executable):
            return on_device_executable
        host_file = context.resolver.get(Executable(NO_ONE, self.abi, 'busybox'))
        return self.install(host_file)

    def list_file_systems(self):
        output = self.execute('mount')
        fstab = []
        for line in output.split('\n'):
            match = FSTAB_ENTRY_REGEX.search(line)
            if match:
                fstab.append(FstabEntry(match.group(1), match.group(2),
                                        match.group(3), match.group(4),
                                        None, None))
            else:  # assume pre-M Android
                fstab.append(FstabEntry(*line.split()))
        return fstab

    # Process query and control

    def get_pids_of(self, process_name):
        raise NotImplementedError()

    def ps(self, **kwargs):
        raise NotImplementedError()

    def kill(self, pid, signal=None, as_root=False):  # pylint: disable=W0221
        """
        Kill the specified process.

            :param pid: PID of the process to kill.
            :param signal: Specify which singal to send to the process. This must
                           be a valid value for -s option of kill. Defaults to ``None``.

        Modified in version 2.1.4: added ``signal`` parameter.

        """
        signal_string = '-s {}'.format(signal) if signal else ''
        self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)

    def killall(self, process_name, signal=None, as_root=False):  # pylint: disable=W0221
        """
        Kill all processes with the specified name.

            :param process_name: The name of the process(es) to kill.
            :param signal: Specify which singal to send to the process. This must
                           be a valid value for -s option of kill. Defaults to ``None``.

        Modified in version 2.1.5: added ``as_root`` parameter.

        """
        for pid in self.get_pids_of(process_name):
            self.kill(pid, signal=signal, as_root=as_root)

    def get_online_cpus(self, c):
        if isinstance(c, int):  # assume c == cluster
            return [i for i in self.online_cpus if self.core_clusters[i] == c]
        elif isinstance(c, basestring):  # assume c == core
            return [i for i in self.online_cpus if self.core_names[i] == c]
        else:
            raise ValueError(c)

    def get_number_of_online_cpus(self, c):
        return len(self.get_online_cpus(c))

    def set_number_of_online_cpus(self, core, number):
        core_ids = [i for i, c in enumerate(self.core_names) if c == core]
        max_cores = len(core_ids)
        if number > max_cores:
            message = 'Attempting to set the number of active {} to {}; maximum is {}'
            raise ValueError(message.format(core, number, max_cores))
        for i in xrange(0, number):
            self.enable_cpu(core_ids[i])
        for i in xrange(number, max_cores):
            self.disable_cpu(core_ids[i])

    # hotplug

    def enable_cpu(self, cpu):
        """
        Enable the specified core.

        :param cpu: CPU core to enable. This must be the full name as it
                    appears in sysfs, e.g. "cpu0".

        """
        self.hotplug_cpu(cpu, online=True)

    def disable_cpu(self, cpu):
        """
        Disable the specified core.

        :param cpu: CPU core to disable. This must be the full name as it
                    appears in sysfs, e.g. "cpu0".
        """
        self.hotplug_cpu(cpu, online=False)

    def hotplug_cpu(self, cpu, online):
        """
        Hotplug the specified CPU either on or off.
        See https://www.kernel.org/doc/Documentation/cpu-hotplug.txt

        :param cpu: The CPU for which the governor is to be set. This must be
                    the full name as it appears in sysfs, e.g. "cpu0".
        :param online: CPU will be enabled if this value bool()'s to True, and
                       will be disabled otherwise.

        """
        if isinstance(cpu, int):
            cpu = 'cpu{}'.format(cpu)
        status = 1 if online else 0
        sysfile = '/sys/devices/system/cpu/{}/online'.format(cpu)
        self.set_sysfile_value(sysfile, status)

    def get_number_of_active_cores(self, core):
        if core not in self.core_names:
            raise ValueError('Unexpected core: {}; must be in {}'.format(core, list(set(self.core_names))))
        active_cpus = self.active_cpus
        num_active_cores = 0
        for i, c in enumerate(self.core_names):
            if c == core and i in active_cpus:
                num_active_cores += 1
        return num_active_cores

    def set_number_of_active_cores(self, core, number):  # NOQA
        if core not in self.core_names:
            raise ValueError('Unexpected core: {}; must be in {}'.format(core, list(set(self.core_names))))
        core_ids = [i for i, c in enumerate(self.core_names) if c == core]
        max_cores = len(core_ids)
        if number > max_cores:
            message = 'Attempting to set the number of active {} to {}; maximum is {}'
            raise ValueError(message.format(core, number, max_cores))

        if not number:
            # make sure at least one other core is enabled to avoid trying to
            # hotplug everything.
            for i, c in enumerate(self.core_names):
                if c != core:
                    self.enable_cpu(i)
                    break
            else:  # did not find one
                raise ValueError('Cannot hotplug all cpus on the device!')

        for i in xrange(0, number):
            self.enable_cpu(core_ids[i])
        for i in xrange(number, max_cores):
            self.disable_cpu(core_ids[i])

    def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
               background=False, as_root=False, timeout=30):
        """
        Executes the specified binary under the specified conditions.

        :binary: binary to execute. Must be present and executable on the device.
        :args: arguments to be passed to the binary. The can be either a list or
               a string.
        :in_directory:  execute the binary in the  specified directory. This must
                        be an absolute path.
        :on_cpus:  taskset the binary to these CPUs. This may be a single ``int`` (in which
                   case, it will be interpreted as the mask), a list of ``ints``, in which
                   case this will be interpreted as the list of cpus, or string, which
                   will be interpreted as a comma-separated list of cpu ranges, e.g.
                   ``"0,4-7"``.
        :background: If ``True``, a ``subprocess.Popen`` object will be returned straight
                     away. If ``False`` (the default), this will wait for the command to
                     terminate and return the STDOUT output
        :as_root: Specify whether the command should be run as root
        :timeout: If the invocation does not terminate within this number of seconds,
                  a ``TimeoutError`` exception will be raised. Set to ``None`` if the
                  invocation should not timeout.

        """
        command = binary
        if args:
            if isiterable(args):
                args = ' '.join(args)
            command = '{} {}'.format(command, args)
        if on_cpus:
            if isinstance(on_cpus, basestring):
                on_cpus = ranges_to_list(on_cpus)
            if isiterable(on_cpus):
                on_cpus = list_to_mask(on_cpus)
            command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
        if in_directory:
            command = 'cd {} && {}'.format(in_directory, command)
        return self.execute(command, background=background, as_root=as_root, timeout=timeout)

    # internal methods

    def _check_ready(self):
        if not self._is_ready:
            raise AttributeError('Device not ready.')

    def _get_core_cluster(self, core):
        """Returns the first cluster that has cores of the specified type. Raises
        value error if no cluster for the specified type has been found"""
        core_indexes = [i for i, c in enumerate(self.core_names) if c == core]
        core_clusters = set(self.core_clusters[i] for i in core_indexes)
        if not core_clusters:
            raise ValueError('No cluster found for core {}'.format(core))
        return sorted(list(core_clusters))[0]
Exemplo n.º 14
0
class ApkWorkload(Workload):
    """
    A workload based on an APK file.

    Defines the following attributes:

    :package: The package name of the app. This is usually a Java-style name of the form
              ``com.companyname.appname``.
    :activity: This is the initial activity of the app. This will be used to launch the
               app during the setup.  Many applications do not specify a launch activity so
               this may be left blank if necessary.
    :view: The class of the main view pane of the app. This needs to be defined in order
           to collect SurfaceFlinger-derived statistics (such as FPS) for the app, but
           may otherwise be left as ``None``.
    :launch_main: If ``False``, the default activity will not be launched (during setup),
                  allowing workloads to start the app with an intent of their choice in
                  the run step. This is useful for apps without a launchable default/main
                  activity or those where it cannot be launched without intent data (which
                  is provided at the run phase).
    :install_timeout: Timeout for the installation of the APK. This may vary wildly based on
                      the size and nature of a specific APK, and so should be defined on
                      per-workload basis.

                      .. note:: To a lesser extent, this will also vary based on the the
                                device and the nature of adb connection (USB vs Ethernet),
                                so, as with all timeouts, so leeway must be included in
                                the specified value.

    :min_apk_version: The minimum supported apk version for this workload. May be ``None``.
    :max_apk_version: The maximum supported apk version for this workload. May be ``None``.

    .. note:: Both package and activity for a workload may be obtained from the APK using
              the ``aapt`` tool that comes with the ADT  (Android Developemnt Tools) bundle.

    """
    package = None
    activity = None
    view = None
    min_apk_version = None
    max_apk_version = None
    supported_platforms = ['android']
    launch_main = True

    parameters = [
        Parameter('install_timeout',
                  kind=int,
                  default=300,
                  description='Timeout for the installation of the apk.'),
        Parameter('check_apk',
                  kind=boolean,
                  default=True,
                  description='''
                  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. 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.'
        ),
        Parameter('exact_abi',
                  kind=bool,
                  default=False,
                  description='''
                  If ``True``, workload will check that the APK matches the target
                  device ABI, otherwise any APK found will be used.
                  '''),
        Parameter('clear_data_on_reset',
                  kind=bool,
                  default=True,
                  description="""
                  If set to ``False``, this will prevent WA from clearing package
                  data for this workload prior to running it.
                  """),
    ]

    def __init__(self, device, _call_super=True, **kwargs):
        if _call_super:
            Workload.__init__(self, device, **kwargs)
        self.apk_file = None
        self.apk_version = None
        self.logcat_log = None
        self.exact_apk_version = None

    def setup(self, context):  # pylint: disable=too-many-branches
        Workload.setup(self, context)
        self.setup_workload_apk(context)
        self.launch_application()
        self.kill_background()
        self.device.clear_logcat()

    def setup_workload_apk(self, context):
        # 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,
                    package=getattr(self, 'package', None)),
            version=getattr(self, 'version', None),
            variant_name=getattr(self, 'variant_name', None),
            strict=False)

        # Get target abi
        target_abi = self.device.get_installed_package_abi(self.package)
        if target_abi:
            self.logger.debug(
                "Found apk with primary abi '{}' on target device".format(
                    target_abi))

        # Get host version, primary abi is first, and then try to find supported.
        for abi in self.device.supported_abi:
            self.apk_file = context.resolver.get(
                ApkFile(self, abi, package=getattr(self, 'package', None)),
                version=getattr(self, 'version', None),
                variant_name=getattr(self, 'variant_name', None),
                strict=False)

            # Stop if apk found, or if exact_abi is set only look for primary abi.
            if self.apk_file or self.exact_abi:
                break

        host_version = self.check_host_version()
        self.verify_apk_version(target_version, target_abi, host_version)

        if self.force_install:
            self.force_install_apk(context, host_version)
        elif self.check_apk:
            self.prefer_host_apk(context, host_version, target_version)
        else:
            self.prefer_target_apk(context, host_version, target_version)

        self.reset(context)
        self.apk_version = self.device.get_installed_package_version(
            self.package)
        context.add_classifiers(apk_version=self.apk_version)

    def check_host_version(self):
        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))
        return host_version

    def verify_apk_version(self, target_version, target_abi, 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))

        if self.exact_apk_version is not None:
            if self.exact_apk_version != target_version and self.exact_apk_version != host_version:
                msg = "APK version '{}' not found on the host '{}' or target '{}'"
                raise ResourceError(
                    msg.format(self.exact_apk_version, host_version,
                               target_version))

        # Error if exact_abi and suitable apk not found on host and incorrect version on device
        if self.exact_abi and host_version is None:
            if target_abi != self.device.abi:
                msg = "APK abi '{}' not found on the host and target is '{}'"
                raise ResourceError(msg.format(self.device.abi, target_abi))

    def launch_application(self):
        if self.launch_main:
            self.launch_package(
            )  # launch default activity without intent data

    def kill_background(self):
        self.device.execute('am kill-all')  # kill all *background* activities

    def force_install_apk(self, context, host_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=True)

    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:
                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:
                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=True)

    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:
            msg += "and a valid version of the app is already on the target, using target app"
            self.logger.debug(msg)

    def validate_version(self, version):
        min_apk_version = getattr(self, 'min_apk_version', None)
        max_apk_version = getattr(self, 'max_apk_version', None)

        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, max_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, max_apk_version))

    def launch_package(self):
        if not self.activity:
            output = self.device.execute('am start -W {}'.format(self.package))
        else:
            output = self.device.execute('am start -W -n {}/{}'.format(
                self.package, self.activity))
        if 'Error:' in output:
            self.device.execute('am force-stop {}'.format(
                self.package))  # this will dismiss any erro dialogs
            raise WorkloadError(output)
        self.logger.debug(output)

    def reset(self, context):  # pylint: disable=W0613
        self.device.execute('am force-stop {}'.format(self.package))
        if self.clear_data_on_reset:
            self.device.execute('pm clear {}'.format(self.package))

        # As of android API level 23, apps can request permissions at runtime,
        # this will grant all of them so requests do not pop up when running the app
        # This can also be done less "manually" during adb install using the -g flag
        if self.device.get_sdk_version() >= 23:
            self._grant_requested_permissions()

    def install_apk(self, context, replace=False):
        success = False
        if replace and self.device.package_is_installed(self.package):
            self.device.uninstall(self.package)
        output = self.device.install_apk(self.apk_file,
                                         timeout=self.install_timeout,
                                         replace=replace,
                                         allow_downgrade=True)
        if 'Failure' in output:
            if 'ALREADY_EXISTS' in output:
                self.logger.warn(
                    'Using already installed APK (did not unistall properly?)')
                self.reset(context)
            else:
                raise WorkloadError(output)
        else:
            self.logger.debug(output)
            success = True
        self.do_post_install(context)
        return success

    def _grant_requested_permissions(self):
        dumpsys_output = self.device.execute(
            command="dumpsys package {}".format(self.package))
        permissions = []
        lines = iter(dumpsys_output.splitlines())
        for line in lines:
            if "requested permissions:" in line:
                break

        for line in lines:
            if "android.permission." in line:
                permissions.append(line.split(":")[0].strip())
            # Matching either of these means the end of requested permissions section
            elif "install permissions:" in line or "runtime permissions:" in line:
                break

        for permission in set(permissions):
            # "Normal" Permisions are automatically granted and cannot be changed
            permission_name = permission.rsplit('.', 1)[1]
            if permission_name not in ANDROID_NORMAL_PERMISSIONS:
                # Some permissions are not allowed to be "changed"
                if permission_name not in ANDROID_UNCHANGEABLE_PERMISSIONS:
                    # On some API 23+ devices, this may fail with a SecurityException
                    # on previously granted permissions. In that case, just skip as it
                    # is not fatal to the workload execution
                    try:
                        self.device.execute("pm grant {} {}".format(
                            self.package, permission))
                    except DeviceError as e:
                        if "changeable permission" in e.message or "Unknown permission" in e.message:
                            self.logger.debug(e)
                        else:
                            raise e

    def do_post_install(self, context):
        """ May be overwritten by derived classes."""
        pass

    def run(self, context):
        pass

    def update_result(self, context):
        self.logcat_log = os.path.join(context.output_directory, 'logcat.log')
        self.device.dump_logcat(self.logcat_log)
        context.add_iteration_artifact(name='logcat',
                                       path='logcat.log',
                                       kind='log',
                                       description='Logact dump for the run.')

    def teardown(self, context):
        self.device.execute('am force-stop {}'.format(self.package))
        if self.uninstall_apk:
            self.device.uninstall(self.package)
Exemplo n.º 15
0
class ApkWorkload(Workload):
    """
    A workload based on an APK file.

    Defines the following attributes:

    :package: The package name of the app. This is usually a Java-style name of the form
              ``com.companyname.appname``.
    :activity: This is the initial activity of the app. This will be used to launch the
               app during the setup.
    :view: The class of the main view pane of the app. This needs to be defined in order
           to collect SurfaceFlinger-derived statistics (such as FPS) for the app, but
           may otherwise be left as ``None``.
    :install_timeout: Timeout for the installation of the APK. This may vary wildly based on
                      the size and nature of a specific APK, and so should be defined on
                      per-workload basis.

                      .. note:: To a lesser extent, this will also vary based on the the
                                device and the nature of adb connection (USB vs Ethernet),
                                so, as with all timeouts, so leeway must be included in
                                the specified value.

    .. note:: Both package and activity for a workload may be obtained from the APK using
              the ``aapt`` tool that comes with the ADT  (Android Developemnt Tools) bundle.

    """
    package = None
    activity = None
    view = None
    install_timeout = None
    default_install_timeout = 300
    supported_platforms = ['android']

    parameters = [
        Parameter('uninstall_apk', kind=boolean, default=False,
                  description="If ``True``, will uninstall workload's APK as part of teardown."),
    ]

    def __init__(self, device, _call_super=True, **kwargs):
        if _call_super:
            super(ApkWorkload, self).__init__(device, **kwargs)
        self.apk_file = None
        self.apk_version = None
        self.logcat_log = None
        self.force_reinstall = kwargs.get('force_reinstall', False)
        if not self.install_timeout:
            self.install_timeout = self.default_install_timeout

    def init_resources(self, context):
        self.apk_file = context.resolver.get(wlauto.common.android.resources.ApkFile(self), version=getattr(self, 'version', None))

    def setup(self, context):
        self.initialize_package(context)
        self.start_activity()
        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)
        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))
            else:
                message = '{} host version: {}, not found on device; installing...'
                self.logger.debug(message.format(os.path.basename(self.apk_file), host_version))
            self.force_reinstall = True
        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_reinstall:
            if installed_version:
                self.device.uninstall(self.package)
            self.install_apk(context)
        else:
            self.reset(context)
        self.apk_version = host_version

    def start_activity(self):
        output = self.device.execute('am start -W -n {}/{}'.format(self.package, self.activity))
        if 'Error:' in output:
            self.device.execute('am force-stop {}'.format(self.package))  # this will dismiss any erro dialogs
            raise WorkloadError(output)
        self.logger.debug(output)

    def reset(self, context):  # pylint: disable=W0613
        self.device.execute('am force-stop {}'.format(self.package))
        self.device.execute('pm clear {}'.format(self.package))

    def install_apk(self, context):
        output = self.device.install(self.apk_file, self.install_timeout)
        if 'Failure' in output:
            if 'ALREADY_EXISTS' in output:
                self.logger.warn('Using already installed APK (did not unistall properly?)')
            else:
                raise WorkloadError(output)
        else:
            self.logger.debug(output)
        self.do_post_install(context)

    def do_post_install(self, context):
        """ May be overwritten by dervied classes."""
        pass

    def run(self, context):
        pass

    def update_result(self, context):
        self.logcat_log = os.path.join(context.output_directory, 'logcat.log')
        self.device.dump_logcat(self.logcat_log)
        context.add_iteration_artifact(name='logcat',
                                       path='logcat.log',
                                       kind='log',
                                       description='Logact dump for the run.')

    def teardown(self, context):
        self.device.execute('am force-stop {}'.format(self.package))
        if self.uninstall_apk:
            self.device.uninstall(self.package)

    def validate(self):
        if not self.apk_file:
            raise WorkloadError('No APK file found for workload {}.'.format(self.name))
Exemplo n.º 16
0
 class DuplicateParamExtension(MyBaseExtension):  # pylint: disable=W0612
     parameters = [
         Parameter('food', override=True, default='cheese'),
     ]
Exemplo n.º 17
0
 class DuplicateParamExtension(MyBaseExtension):  # pylint: disable=W0612
     parameters = [
         Parameter('base', override=True, default='buttery'),
         Parameter('base', override=True, default='biscuit'),
     ]
Exemplo n.º 18
0
 class OverridingExtension(MyBaseExtension):  # pylint: disable=W0612
     parameters = [
         Parameter('base', override=True, default='cheese'),
     ]
Exemplo n.º 19
0
 class BadExtension(MyBaseExtension):  # pylint: disable=W0612
     parameters = [
         Parameter('base'),
     ]
Exemplo n.º 20
0
class GameWorkload(ApkWorkload, ReventWorkload):
    """
    GameWorkload is the base class for all the workload that use revent files to
    run.

    For more in depth details on how to record revent files, please see
    :ref:`revent_files_creation`. To subclass this class, please refer to
    :ref:`GameWorkload`.

    Additionally, this class defines the following attributes:

    :asset_file: A tarball containing additional assets for the workload. These are the assets
                 that are not part of the APK but would need to be downloaded by the workload
                 (usually, on first run of the app). Since the presence of a network connection
                 cannot be assumed on some devices, this provides an alternative means of obtaining
                 the assets.
    :saved_state_file: A tarball containing the saved state for a workload. This tarball gets
                       deployed in the same way as the asset file. The only difference being that
                       it is usually much slower and re-deploying the tarball should alone be
                       enough to reset the workload to a known state (without having to reinstall
                       the app or re-deploy the other assets).
    :loading_time: Time it takes for the workload to load after the initial activity has been
                   started.

    """

    # May be optionally overwritten by subclasses
    asset_file = None
    saved_state_file = None
    view = 'SurfaceView'
    install_timeout = 500
    loading_time = 10
    supported_platforms = ['android']

    parameters = [
        Parameter('clear_data_on_reset', kind=bool, default=True,
                  description="""
                  If set to ``False``, this will prevent WA from clearing package
                  data for this workload prior to running it.
                  """),
    ]

    def __init__(self, device, **kwargs):  # pylint: disable=W0613
        ApkWorkload.__init__(self, device, **kwargs)
        ReventWorkload.__init__(self, device, _call_super=False, **kwargs)
        self.logcat_process = None
        self.module_dir = os.path.dirname(sys.modules[self.__module__].__file__)
        self.revent_dir = os.path.join(self.module_dir, 'revent_files')

    def init_resources(self, context):
        ApkWorkload.init_resources(self, context)
        ReventWorkload.init_resources(self, context)

    def setup(self, context):
        ApkWorkload.setup(self, context)
        self.logger.debug('Waiting for the game to load...')
        time.sleep(self.loading_time)
        ReventWorkload.setup(self, context)

    def do_post_install(self, context):
        ApkWorkload.do_post_install(self, context)
        self._deploy_assets(context)

    def reset(self, context):
        # If saved state exists, restore it; if not, do full
        # uninstall/install cycle.
        self.device.execute('am force-stop {}'.format(self.package))
        if self.saved_state_file:
            self._deploy_resource_tarball(context, self.saved_state_file)
        else:
            if self.clear_data_on_reset:
                self.device.execute('pm clear {}'.format(self.package))
            self._deploy_assets(context)

    def run(self, context):
        ReventWorkload.run(self, context)

    def teardown(self, context):
        if not self.saved_state_file:
            ApkWorkload.teardown(self, context)
        else:
            self.device.execute('am force-stop {}'.format(self.package))
        ReventWorkload.teardown(self, context)

    def _deploy_assets(self, context, timeout=300):
        if self.asset_file:
            self._deploy_resource_tarball(context, self.asset_file, timeout)
        if self.saved_state_file:  # must be deployed *after* asset tarball!
            self._deploy_resource_tarball(context, self.saved_state_file, timeout)

    def _deploy_resource_tarball(self, context, resource_file, timeout=300):
        kind = 'data'
        if ':' in resource_file:
            kind, resource_file = resource_file.split(':', 1)
        ondevice_cache = self.device.path.join(self.device.resource_cache, self.name, resource_file)
        if not self.device.file_exists(ondevice_cache):
            asset_tarball = context.resolver.get(ExtensionAsset(self, resource_file))
            if not asset_tarball:
                message = 'Could not find resource {} for workload {}.'
                raise WorkloadError(message.format(resource_file, self.name))
            # adb push will create intermediate directories if they don't
            # exist.
            self.device.push_file(asset_tarball, ondevice_cache)

        device_asset_directory = self.device.path.join(self.device.external_storage_directory, 'Android', kind)
        deploy_command = 'cd {} && {} tar -xzf {}'.format(device_asset_directory,
                                                          self.device.busybox,
                                                          ondevice_cache)
        self.device.execute(deploy_command, timeout=timeout, as_root=True)
Exemplo n.º 21
0
class Device(Extension):
    """
    Base class for all devices supported by Workload Automation. Defines
    the interface the rest of WA uses to interact with devices.

        :name: Unique name used to identify the device.
        :platform: The name of the device's platform (e.g. ``Android``) this may
                   be used by workloads and instrumentation to assess whether they
                   can run on the device.
        :working_directory: a string of the directory which is
                            going to be used by the workloads on the device.
        :binaries_directory: a string of the binary directory for
                             the device.
        :has_gpu:     Should be ``True`` if the device as a separate GPU, and
                    ``False`` if graphics processing is done on a CPU.

                    .. note:: Pretty much all devices currently on the market
                                have GPUs, however this may not be the case for some
                                development boards.

        :path_module: The name of one of the modules implementing the os.path
                      interface, e.g. ``posixpath`` or ``ntpath``. You can provide
                      your own implementation rather than relying on one of the
                      standard library modules, in which case you need to specify
                      the *full* path to you module. e.g. '/home/joebloggs/mypathimp.py'
        :parameters: A list of RuntimeParameter objects. The order of the objects
                     is very important as the setters and getters will be called
                     in the order the RuntimeParameter objects inserted.
        :active_cores: This should be a list of all the currently active cpus in
                      the device in ``'/sys/devices/system/cpu/online'``. The
                      returned list should be read from the device at the time
                      of read request.

    """
    __metaclass__ = DeviceMeta

    parameters = [
        Parameter('core_names',
                  kind=list_of(caseless_string),
                  mandatory=True,
                  default=None,
                  description="""
                  This is a list of all cpu cores on the device with each
                  element being the core type, e.g. ``['a7', 'a7', 'a15']``. The
                  order of the cores must match the order they are listed in
                  ``'/sys/devices/system/cpu'``. So in this case, ``'cpu0'`` must
                  be an A7 core, and ``'cpu2'`` an A15.'
                  """),
        Parameter('core_clusters',
                  kind=list_of_integers,
                  mandatory=True,
                  default=None,
                  description="""
                  This is a list indicating the cluster affinity of the CPU cores,
                  each element correponding to the cluster ID of the core coresponding
                  to it's index. E.g. ``[0, 0, 1]`` indicates that cpu0 and cpu1 are on
                  cluster 0, while cpu2 is on cluster 1. If this is not specified, this
                  will be inferred from ``core_names`` if possible (assuming all cores with
                  the same name are on the same cluster).
                  """),
    ]

    runtime_parameters = []
    # dynamic modules are loaded or not based on whether the device supports
    # them (established at runtime by module probling the device).
    dynamic_modules = []

    # These must be overwritten by subclasses.
    name = None
    platform = None
    default_working_directory = None
    has_gpu = None
    path_module = None
    active_cores = None

    def __init__(self, **kwargs):  # pylint: disable=W0613
        super(Device, self).__init__(**kwargs)
        if not self.path_module:
            raise NotImplementedError(
                'path_module must be specified by the deriving classes.')
        libpath = os.path.dirname(os.__file__)
        modpath = os.path.join(libpath, self.path_module)
        if not modpath.lower().endswith('.py'):
            modpath += '.py'
        try:
            self.path = imp.load_source('device_path', modpath)
        except IOError:
            raise DeviceError('Unsupported path module: {}'.format(
                self.path_module))

    def validate(self):
        # pylint: disable=access-member-before-definition,attribute-defined-outside-init
        if self.core_names and not self.core_clusters:
            self.core_clusters = []
            clusters = []
            for cn in self.core_names:
                if cn not in clusters:
                    clusters.append(cn)
                self.core_clusters.append(clusters.index(cn))
        if len(self.core_names) != len(self.core_clusters):
            raise ConfigError(
                'core_names and core_clusters are of different lengths.')

    def initialize(self, context):
        """
        Initialization that is performed at the begining of the run (after the device has
        been connecte).

        """
        loader = ExtensionLoader()
        for module_spec in self.dynamic_modules:
            module = self._load_module(loader, module_spec)
            if not hasattr(module, 'probe'):
                message = 'Module {} does not have "probe" attribute; cannot be loaded dynamically'
                raise ValueError(message.format(module.name))
            if module.probe(self):
                self.logger.debug('Installing module "{}"'.format(module.name))
                self._install_module(module)
            else:
                self.logger.debug(
                    'Module "{}" is not supported by the device'.format(
                        module.name))

    def reset(self):
        """
        Initiate rebooting of the device.

        Added in version 2.1.3.

        """
        raise NotImplementedError()

    def boot(self, *args, **kwargs):
        """
        Perform the seteps necessary to boot the device to the point where it is ready
        to accept other commands.

        Changed in version 2.1.3: no longer expected to wait until boot completes.

        """
        raise NotImplementedError()

    def connect(self, *args, **kwargs):
        """
        Establish a connection to the device that will be used for subsequent commands.

        Added in version 2.1.3.
        """
        raise NotImplementedError()

    def disconnect(self):
        """ Close the established connection to the device. """
        raise NotImplementedError()

    def ping(self):
        """
        This must return successfully if the device is able to receive commands, or must
        raise :class:`wlauto.exceptions.DeviceUnresponsiveError` if the device cannot respond.

        """
        raise NotImplementedError()

    def get_runtime_parameter_names(self):
        return [p.name for p in self._expand_runtime_parameters()]

    def get_runtime_parameters(self):
        """ returns the runtime parameters that have been set. """
        # pylint: disable=cell-var-from-loop
        runtime_parameters = OrderedDict()
        for rtp in self._expand_runtime_parameters():
            if not rtp.getter:
                continue
            getter = getattr(self, rtp.getter)
            rtp_value = getter(**rtp.getter_args)
            runtime_parameters[rtp.name] = rtp_value
        return runtime_parameters

    def set_runtime_parameters(self, params):
        """
        The parameters are taken from the keyword arguments and are specific to
        a particular device. See the device documentation.

        """
        runtime_parameters = self._expand_runtime_parameters()
        rtp_map = {rtp.name.lower(): rtp for rtp in runtime_parameters}

        params = OrderedDict(
            (k.lower(), v) for k, v in params.iteritems() if v is not None)

        expected_keys = rtp_map.keys()
        if not set(params.keys()) <= set(expected_keys):
            unknown_params = list(
                set(params.keys()).difference(set(expected_keys)))
            raise ConfigError(
                'Unknown runtime parameter(s): {}'.format(unknown_params))

        for param in params:
            self.logger.debug('Setting runtime parameter "{}"'.format(param))
            rtp = rtp_map[param]
            setter = getattr(self, rtp.setter)
            args = dict(rtp.setter_args.items() +
                        [(rtp.value_name, params[rtp.name.lower()])])
            setter(**args)

    def capture_screen(self, filepath):
        """Captures the current device screen into the specified file in a PNG format."""
        raise NotImplementedError()

    def get_properties(self, output_path):
        """Captures and saves the device configuration properties version and
         any other relevant information. Return them in a dict"""
        raise NotImplementedError()

    def listdir(self, path, **kwargs):
        """ List the contents of the specified directory. """
        raise NotImplementedError()

    def push_file(self, source, dest):
        """ Push a file from the host file system onto the device. """
        raise NotImplementedError()

    def pull_file(self, source, dest):
        """ Pull a file from device system onto the host file system. """
        raise NotImplementedError()

    def delete_file(self, filepath):
        """ Delete the specified file on the device. """
        raise NotImplementedError()

    def file_exists(self, filepath):
        """ Check if the specified file or directory exist on the device. """
        raise NotImplementedError()

    def get_pids_of(self, process_name):
        """ Returns a list of PIDs of the specified process name. """
        raise NotImplementedError()

    def kill(self, pid, as_root=False):
        """ Kill the  process with the specified PID. """
        raise NotImplementedError()

    def killall(self, process_name, as_root=False):
        """ Kill all running processes with the specified name. """
        raise NotImplementedError()

    def install(self, filepath, **kwargs):
        """ Install the specified file on the device. What "install" means is device-specific
        and may possibly also depend on the type of file."""
        raise NotImplementedError()

    def uninstall(self, filepath):
        """ Uninstall the specified file on the device. What "uninstall" means is device-specific
        and may possibly also depend on the type of file."""
        raise NotImplementedError()

    def execute(self, command, timeout=None, **kwargs):
        """
        Execute the specified command command on the device and return the output.

        :param command: Command to be executed on the device.
        :param timeout: If the command does not return after the specified time,
                        execute() will abort with an error. If there is no timeout for
                        the command, this should be set to 0 or None.

        Other device-specific keyword arguments may also be specified.

        :returns: The stdout output from the command.

        """
        raise NotImplementedError()

    def set_sysfile_value(self, filepath, value, verify=True):
        """
        Write the specified value to the specified file on the device
        and verify that the value has actually been written.

        :param file: The file to be modified.
        :param value: The value to be written to the file. Must be
                      an int or a string convertable to an int.
        :param verify: Specifies whether the value should be verified, once written.

        Should raise DeviceError if could write value.

        """
        raise NotImplementedError()

    def get_sysfile_value(self, sysfile, kind=None):
        """
        Get the contents of the specified sysfile.

        :param sysfile: The file who's contents will be returned.

        :param kind: The type of value to be expected in the sysfile. This can
                     be any Python callable that takes a single str argument.
                     If not specified or is None, the contents will be returned
                     as a string.

        """
        raise NotImplementedError()

    def start(self):
        """
        This gets invoked before an iteration is started and is endented to help the
        device manange any internal supporting functions.

        """
        pass

    def stop(self):
        """
        This gets invoked after iteration execution has completed and is endented to help the
        device manange any internal supporting functions.

        """
        pass

    def __str__(self):
        return 'Device<{}>'.format(self.name)

    __repr__ = __str__

    def _expand_runtime_parameters(self):
        expanded_params = []
        for param in self.runtime_parameters:
            if isinstance(param, CoreParameter):
                expanded_params.extend(
                    param.get_runtime_parameters(self.core_names))  # pylint: disable=no-member
            else:
                expanded_params.append(param)
        return expanded_params

    @contextmanager
    def _check_alive(self):
        try:
            yield
        except Exception as e:
            self.ping()
            raise e
Exemplo n.º 22
0
class GameWorkload(ApkWorkload, ReventWorkload):
    """
    GameWorkload is the base class for all the workload that use revent files to
    run.

    For more in depth details on how to record revent files, please see
    :ref:`revent_files_creation`. To subclass this class, please refer to
    :ref:`GameWorkload`.

    Additionally, this class defines the following attributes:

    :asset_file: A tarball containing additional assets for the workload. These are the assets
                 that are not part of the APK but would need to be downloaded by the workload
                 (usually, on first run of the app). Since the presence of a network connection
                 cannot be assumed on some devices, this provides an alternative means of obtaining
                 the assets.
    :saved_state_file: A tarball containing the saved state for a workload. This tarball gets
                       deployed in the same way as the asset file. The only difference being that
                       it is usually much slower and re-deploying the tarball should alone be
                       enough to reset the workload to a known state (without having to reinstall
                       the app or re-deploy the other assets).
    :loading_time: Time it takes for the workload to load after the initial activity has been
                   started.

    """

    # May be optionally overwritten by subclasses
    asset_file = None
    saved_state_file = None
    view = 'SurfaceView'
    loading_time = 10
    supported_platforms = ['android']
    setup_required = True

    parameters = [
        Parameter('install_timeout', default=500, override=True),
        Parameter(
            'check_states',
            kind=bool,
            default=False,
            global_alias='check_game_states',
            description=
            """Use visual state detection to verify the state of the workload
                  after setup and run"""),
        Parameter(
            'assets_push_timeout',
            kind=int,
            default=500,
            description=
            'Timeout used during deployment of the assets package (if there is one).'
        ),
    ]

    def __init__(self, device, **kwargs):  # pylint: disable=W0613
        ApkWorkload.__init__(self, device, **kwargs)
        ReventWorkload.__init__(self, device, _call_super=False, **kwargs)
        if self.check_states:
            state_detector.check_match_state_dependencies()
        self.logcat_process = None
        self.module_dir = os.path.dirname(
            sys.modules[self.__module__].__file__)
        self.revent_dir = os.path.join(self.module_dir, 'revent_files')

    def init_resources(self, context):
        ApkWorkload.init_resources(self, context)
        ReventWorkload.init_resources(self, context)
        if self.check_states:
            self._check_statedetection_files(context)

    def setup(self, context):
        ApkWorkload.setup(self, context)
        self.logger.debug('Waiting for the game to load...')
        time.sleep(self.loading_time)
        ReventWorkload.setup(self, context)

        # state detection check if it's enabled in the config
        if self.check_states:
            self.check_state(context, "setup_complete")

    def do_post_install(self, context):
        ApkWorkload.do_post_install(self, context)
        self._deploy_assets(context, self.assets_push_timeout)

    def reset(self, context):
        # If saved state exists, restore it; if not, do full
        # uninstall/install cycle.
        self.device.execute('am force-stop {}'.format(self.package))
        if self.saved_state_file:
            self._deploy_resource_tarball(context, self.saved_state_file)
        else:
            if self.clear_data_on_reset:
                self.device.execute('pm clear {}'.format(self.package))
            self._deploy_assets(context)

    def run(self, context):
        ReventWorkload.run(self, context)

    def teardown(self, context):
        # state detection check if it's enabled in the config
        if self.check_states:
            self.check_state(context, "run_complete")

        if not self.saved_state_file:
            ApkWorkload.teardown(self, context)
        else:
            self.device.execute('am force-stop {}'.format(self.package))
        ReventWorkload.teardown(self, context)

    def _deploy_assets(self, context, timeout=300):
        if self.asset_file:
            self._deploy_resource_tarball(context, self.asset_file, timeout)
        if self.saved_state_file:  # must be deployed *after* asset tarball!
            self._deploy_resource_tarball(context, self.saved_state_file,
                                          timeout)

    def _deploy_resource_tarball(self, context, resource_file, timeout=300):
        kind = 'data'
        if ':' in resource_file:
            kind, resource_file = resource_file.split(':', 1)
        ondevice_cache = self.device.path.join(self.device.resource_cache,
                                               self.name, resource_file)
        if not self.device.file_exists(ondevice_cache):
            asset_tarball = context.resolver.get(
                ExtensionAsset(self, resource_file))
            if not asset_tarball:
                message = 'Could not find resource {} for workload {}.'
                raise WorkloadError(message.format(resource_file, self.name))
            # adb push will create intermediate directories if they don't
            # exist.
            self.device.push_file(asset_tarball,
                                  ondevice_cache,
                                  timeout=timeout)

        device_asset_directory = self.device.path.join(
            self.device.external_storage_directory, 'Android', kind)
        deploy_command = 'cd {} && {} tar -xzf {}'.format(
            device_asset_directory, self.device.busybox, ondevice_cache)
        self.device.execute(deploy_command, timeout=timeout, as_root=True)

    def _check_statedetection_files(self, context):
        try:
            self.statedefs_dir = context.resolver.get(
                File(self, 'state_definitions'))
        except ResourceError:
            self.logger.warning(
                "State definitions directory not found. Disabling state detection."
            )
            self.check_states = False  # pylint: disable=W0201

    def check_state(self, context, phase):
        try:
            self.logger.info("\tChecking workload state...")
            screenshotPath = os.path.join(context.output_directory,
                                          "screen.png")
            self.device.capture_screen(screenshotPath)
            stateCheck = state_detector.verify_state(screenshotPath,
                                                     self.statedefs_dir, phase)
            if not stateCheck:
                raise WorkloadError("Unexpected state after setup")
        except state_detector.StateDefinitionError as e:
            msg = "State definitions or template files missing or invalid ({}). Skipping state detection."
            self.logger.warning(msg.format(e.message))
Exemplo n.º 23
0
class AndroidDevice(BaseLinuxDevice):  # pylint: disable=W0223
    """
    Device running Android OS.

    """

    platform = 'android'

    parameters = [
        Parameter('adb_name',
                  description=
                  'The unique ID of the device as output by "adb devices".'),
        Parameter(
            'android_prompt',
            kind=regex,
            default=re.compile('^.*(shell|root)@.*:/\S* [#$] ', re.MULTILINE),
            description='The format  of matching the shell prompt in Android.'
        ),
        Parameter('working_directory',
                  default='/sdcard/wa-working',
                  override=True),
        Parameter('binaries_directory',
                  default='/data/local/tmp/wa-bin',
                  override=True,
                  description='Location of binaries on the device.'),
        Parameter(
            'package_data_directory',
            default='/data/data',
            description='Location of of data for an installed package (APK).'),
        Parameter('external_storage_directory',
                  default='/sdcard',
                  description='Mount point for external storage.'),
        Parameter('connection',
                  default='usb',
                  allowed_values=['usb', 'ethernet'],
                  description='Specified the nature of adb connection.'),
        Parameter('logcat_poll_period',
                  kind=int,
                  description="""
                  If specified and is not ``0``, logcat will be polled every
                  ``logcat_poll_period`` seconds, and buffered on the host. This
                  can be used if a lot of output is expected in logcat and the fixed
                  logcat buffer on the device is not big enough. The trade off is that
                  this introduces some minor runtime overhead. Not set by default.
                  """),
        Parameter('enable_screen_check',
                  kind=boolean,
                  default=False,
                  description="""
                  Specified whether the device should make sure that the screen is on
                  during initialization.
                  """),
        Parameter('swipe_to_unlock',
                  kind=str,
                  default=None,
                  allowed_values=[None, "horizontal", "vertical"],
                  description="""
                  If set a swipe of the specified direction will be performed.
                  This should unlock the screen.
                  """),
    ]

    default_timeout = 30
    delay = 2
    long_delay = 3 * delay
    ready_timeout = 60

    # Overwritten from Device. For documentation, see corresponding method in
    # Device.

    @property
    def is_rooted(self):
        if self._is_rooted is None:
            try:
                result = adb_shell(self.adb_name, 'su', timeout=1)
                if 'not found' in result:
                    self._is_rooted = False
                else:
                    self._is_rooted = True
            except TimeoutError:
                self._is_rooted = True
            except DeviceError:
                self._is_rooted = False
        return self._is_rooted

    @property
    def abi(self):
        val = self.getprop()['ro.product.cpu.abi'].split('-')[0]
        for abi, architectures in ABI_MAP.iteritems():
            if val in architectures:
                return abi
        return val

    @property
    def supported_abi(self):
        props = self.getprop()
        result = [props['ro.product.cpu.abi']]
        if 'ro.product.cpu.abi2' in props:
            result.append(props['ro.product.cpu.abi2'])
        if 'ro.product.cpu.abilist' in props:
            for abi in props['ro.product.cpu.abilist'].split(','):
                if abi not in result:
                    result.append(abi)

        mapped_result = []
        for supported_abi in result:
            for abi, architectures in ABI_MAP.iteritems():
                found = False
                if supported_abi in architectures and abi not in mapped_result:
                    mapped_result.append(abi)
                    found = True
                    break
            if not found and supported_abi not in mapped_result:
                mapped_result.append(supported_abi)
        return mapped_result

    def __init__(self, **kwargs):
        super(AndroidDevice, self).__init__(**kwargs)
        self._logcat_poller = None

    def reset(self):
        self._is_ready = False
        self._just_rebooted = True
        adb_command(self.adb_name, 'reboot', timeout=self.default_timeout)

    def hard_reset(self):
        super(AndroidDevice, self).hard_reset()
        self._is_ready = False
        self._just_rebooted = True

    def boot(self, hard=False, **kwargs):
        if hard:
            self.hard_reset()
        else:
            self.reset()

    def connect(self):  # NOQA pylint: disable=R0912
        iteration_number = 0
        max_iterations = self.ready_timeout / self.delay
        available = False
        self.logger.debug('Polling for device {}...'.format(self.adb_name))
        while iteration_number < max_iterations:
            devices = adb_list_devices()
            if self.adb_name:
                for device in devices:
                    if device.name == self.adb_name and device.status != 'offline':
                        available = True
            else:  # adb_name not set
                if len(devices) == 1:
                    available = True
                elif len(devices) > 1:
                    raise DeviceError(
                        'More than one device is connected and adb_name is not set.'
                    )

            if available:
                break
            else:
                time.sleep(self.delay)
                iteration_number += 1
        else:
            raise DeviceError('Could not boot {} ({}).'.format(
                self.name, self.adb_name))

        while iteration_number < max_iterations:
            available = (int('0' +
                             (adb_shell(self.adb_name,
                                        'getprop sys.boot_completed',
                                        timeout=self.default_timeout))) == 1)
            if available:
                break
            else:
                time.sleep(self.delay)
                iteration_number += 1
        else:
            raise DeviceError('Could not boot {} ({}).'.format(
                self.name, self.adb_name))

        if self._just_rebooted:
            self.logger.debug('Waiting for boot to complete...')
            # On some devices, adb connection gets reset some time after booting.
            # This  causes errors during execution. To prevent this, open a shell
            # session and wait for it to be killed. Once its killed, give adb
            # enough time to restart, and then the device should be ready.
            # TODO: This is more of a work-around rather than an actual solution.
            #       Need to figure out what is going on the "proper" way of handling it.
            try:
                adb_shell(self.adb_name, '', timeout=20)
                time.sleep(5)  # give adb time to re-initialize
            except TimeoutError:
                pass  # timed out waiting for the session to be killed -- assume not going to be.

            self.logger.debug('Boot completed.')
            self._just_rebooted = False
        self._is_ready = True

    def initialize(self, context):
        self.sqlite = self.deploy_sqlite3(context)  # pylint: disable=attribute-defined-outside-init
        if self.is_rooted:
            self.disable_screen_lock()
            self.disable_selinux()
        if self.enable_screen_check:
            self.ensure_screen_is_on()

    def disconnect(self):
        if self._logcat_poller:
            self._logcat_poller.close()

    def ping(self):
        try:
            # May be triggered inside initialize()
            adb_shell(self.adb_name, 'ls /', timeout=10)
        except (TimeoutError, CalledProcessError):
            raise DeviceNotRespondingError(self.adb_name or self.name)

    def start(self):
        if self.logcat_poll_period:
            if self._logcat_poller:
                self._logcat_poller.close()
            self._logcat_poller = _LogcatPoller(self,
                                                self.logcat_poll_period,
                                                timeout=self.default_timeout)
            self._logcat_poller.start()

    def stop(self):
        if self._logcat_poller:
            self._logcat_poller.stop()

    def get_android_version(self):
        return ANDROID_VERSION_MAP.get(self.get_sdk_version(), None)

    def get_android_id(self):
        """
        Get the device's ANDROID_ID. Which is

            "A 64-bit number (as a hex string) that is randomly generated when the user
            first sets up the device and should remain constant for the lifetime of the
            user's device."

        .. note:: This will get reset on userdata erasure.

        """
        output = self.execute(
            'content query --uri content://settings/secure --projection value --where "name=\'android_id\'"'
        ).strip()
        return output.split('value=')[-1]

    def get_sdk_version(self):
        try:
            return int(self.getprop('ro.build.version.sdk'))
        except (ValueError, TypeError):
            return None

    def get_installed_package_version(self, package):
        """
        Returns the version (versionName) of the specified package if it is installed
        on the device, or ``None`` otherwise.

        Added in version 2.1.4

        """
        output = self.execute('dumpsys package {}'.format(package))
        for line in convert_new_lines(output).split('\n'):
            if 'versionName' in line:
                return line.split('=', 1)[1]
        return None

    def get_installed_package_abi(self, package):
        """
        Returns the primary abi of the specified package if it is installed
        on the device, or ``None`` otherwise.
        """
        output = self.execute('dumpsys package {}'.format(package))
        val = None
        for line in convert_new_lines(output).split('\n'):
            if 'primaryCpuAbi' in line:
                val = line.split('=', 1)[1]
                break
        if val == 'null':
            return None
        for abi, architectures in ABI_MAP.iteritems():
            if val in architectures:
                return abi
        return val

    def list_packages(self):
        """
        List packages installed on the device.

        Added in version 2.1.4

        """
        output = self.execute('pm list packages')
        output = output.replace('package:', '')
        return output.split()

    def package_is_installed(self, package_name):
        """
        Returns ``True`` the if a package with the specified name is installed on
        the device, and ``False`` otherwise.

        Added in version 2.1.4

        """
        return package_name in self.list_packages()

    def executable_is_installed(self, executable_name):  # pylint: disable=unused-argument,no-self-use
        raise AttributeError("""Instead of using is_installed, please use
            ``get_binary_path`` or ``install_if_needed`` instead. You should
            use the path returned by these functions to then invoke the binary

            please see: https://pythonhosted.org/wlauto/writing_extensions.html"""
                             )

    def is_installed(self, name):
        if self.package_is_installed(name):
            return True
        elif "." in name:  # assumes android packages have a . in their name and binaries documentation
            return False
        else:
            raise AttributeError("""Instead of using is_installed, please use
                ``get_binary_path`` or ``install_if_needed`` instead. You should
                use the path returned by these functions to then invoke the binary

                please see: https://pythonhosted.org/wlauto/writing_extensions.html"""
                                 )

    def listdir(self, path, as_root=False, **kwargs):
        contents = self.execute('ls {}'.format(path), as_root=as_root)
        return [x.strip() for x in contents.split()]

    def push_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221
        """
        Modified in version 2.1.4: added  ``as_root`` parameter.

        """
        self._check_ready()
        try:
            if not as_root:
                adb_command(self.adb_name,
                            "push '{}' '{}'".format(source, dest),
                            timeout=timeout)
            else:
                device_tempfile = self.path.join(self.file_transfer_cache,
                                                 source.lstrip(self.path.sep))
                self.execute('mkdir -p {}'.format(
                    self.path.dirname(device_tempfile)))
                adb_command(self.adb_name,
                            "push '{}' '{}'".format(source, device_tempfile),
                            timeout=timeout)
                self.execute('cp {} {}'.format(device_tempfile, dest),
                             as_root=True)
        except CalledProcessError as e:
            raise DeviceError(e)

    def pull_file(self, source, dest, as_root=False, timeout=default_timeout):  # pylint: disable=W0221
        """
        Modified in version 2.1.4: added  ``as_root`` parameter.

        """
        self._check_ready()
        try:
            if not as_root:
                adb_command(self.adb_name,
                            "pull '{}' '{}'".format(source, dest),
                            timeout=timeout)
            else:
                device_tempfile = self.path.join(self.file_transfer_cache,
                                                 source.lstrip(self.path.sep))
                self.execute('mkdir -p {}'.format(
                    self.path.dirname(device_tempfile)))
                self.execute('cp {} {}'.format(source, device_tempfile),
                             as_root=True)
                adb_command(self.adb_name,
                            "pull '{}' '{}'".format(device_tempfile, dest),
                            timeout=timeout)
        except CalledProcessError as e:
            raise DeviceError(e)

    def delete_file(self, filepath, as_root=False):  # pylint: disable=W0221
        self._check_ready()
        adb_shell(self.adb_name,
                  "rm -rf '{}'".format(filepath),
                  as_root=as_root,
                  timeout=self.default_timeout)

    def file_exists(self, filepath):
        self._check_ready()
        output = adb_shell(
            self.adb_name,
            'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath),
            timeout=self.default_timeout)
        return bool(int(output))

    def install(self,
                filepath,
                timeout=default_timeout,
                with_name=None,
                replace=False):  # pylint: disable=W0221
        ext = os.path.splitext(filepath)[1].lower()
        if ext == '.apk':
            return self.install_apk(filepath, timeout, replace)
        else:
            return self.install_executable(filepath, with_name)

    def install_apk(self,
                    filepath,
                    timeout=default_timeout,
                    replace=False,
                    allow_downgrade=False):  # pylint: disable=W0221
        self._check_ready()
        ext = os.path.splitext(filepath)[1].lower()
        if ext == '.apk':
            flags = []
            if replace:
                flags.append('-r')  # Replace existing APK
            if allow_downgrade:
                flags.append(
                    '-d'
                )  # Install the APK even if a newer version is already installed
            if self.get_sdk_version() >= 23:
                flags.append('-g')  # Grant all runtime permissions
            self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(
                replace, ' '.join(flags)))
            return adb_command(self.adb_name,
                               "install {} '{}'".format(
                                   ' '.join(flags), filepath),
                               timeout=timeout)
        else:
            raise DeviceError(
                'Can\'t install {}: unsupported format.'.format(filepath))

    def install_executable(self, filepath, with_name=None):
        """
        Installs a binary executable on device. Returns
        the path to the installed binary, or ``None`` if the installation has failed.
        Optionally, ``with_name`` parameter may be used to specify a different name under
        which the executable will be installed.

        Added in version 2.1.3.
        Updated in version 2.1.5 with ``with_name`` parameter.

        """
        self._ensure_binaries_directory_is_writable()
        executable_name = with_name or os.path.basename(filepath)
        on_device_file = self.path.join(self.working_directory,
                                        executable_name)
        on_device_executable = self.path.join(self.binaries_directory,
                                              executable_name)
        self.push_file(filepath, on_device_file)
        self.execute('cp {} {}'.format(on_device_file, on_device_executable),
                     as_root=self.is_rooted)
        self.execute('chmod 0777 {}'.format(on_device_executable),
                     as_root=self.is_rooted)
        return on_device_executable

    def uninstall(self, package):
        self._check_ready()
        adb_command(self.adb_name,
                    "uninstall {}".format(package),
                    timeout=self.default_timeout)

    def uninstall_executable(self, executable_name):
        """

        Added in version 2.1.3.

        """
        on_device_executable = self.get_binary_path(
            executable_name, search_system_binaries=False)
        if not on_device_executable:
            raise DeviceError(
                "Could not uninstall {}, binary not found".format(
                    on_device_executable))
        self._ensure_binaries_directory_is_writable()
        self.delete_file(on_device_executable, as_root=self.is_rooted)

    def execute(self,
                command,
                timeout=default_timeout,
                check_exit_code=True,
                background=False,
                as_root=False,
                busybox=False,
                **kwargs):
        """
        Execute the specified command on the device using adb.

        Parameters:

            :param command: The command to be executed. It should appear exactly
                            as if you were typing it into a shell.
            :param timeout: Time, in seconds, to wait for adb to return before aborting
                            and raising an error. Defaults to ``AndroidDevice.default_timeout``.
            :param check_exit_code: If ``True``, the return code of the command on the Device will
                                    be check and exception will be raised if it is not 0.
                                    Defaults to ``True``.
            :param background: If ``True``, will execute adb in a subprocess, and will return
                               immediately, not waiting for adb to return. Defaults to ``False``
            :param busybox: If ``True``, will use busybox to execute the command. Defaults to ``False``.

                            Added in version 2.1.3

                            .. note:: The device must be rooted to be able to use some busybox features.

            :param as_root: If ``True``, will attempt to execute command in privileged mode. The device
                            must be rooted, otherwise an error will be raised. Defaults to ``False``.

                            Added in version 2.1.3

        :returns: If ``background`` parameter is set to ``True``, the subprocess object will
                  be returned; otherwise, the contents of STDOUT from the device will be returned.

        :raises: DeviceError if adb timed out  or if the command returned non-zero exit
                 code on the device, or if attempting to execute a command in privileged mode on an
                 unrooted device.

        """
        self._check_ready()
        if as_root and not self.is_rooted:
            raise DeviceError(
                'Attempting to execute "{}" as root on unrooted device.'.
                format(command))
        if busybox:
            command = ' '.join([self.busybox, command])
        if background:
            return adb_background_shell(self.adb_name,
                                        command,
                                        as_root=as_root)
        else:
            return adb_shell(self.adb_name, command, timeout, check_exit_code,
                             as_root)

    def kick_off(self, command, as_root=None):
        """
        Like execute but closes adb session and returns immediately, leaving the command running on the
        device (this is different from execute(background=True) which keeps adb connection open and returns
        a subprocess object).

        Added in version 2.1.4

        """
        if as_root is None:
            as_root = self.is_rooted
        try:
            command = 'cd {} && {} nohup {}'.format(self.working_directory,
                                                    self.busybox, command)
            output = self.execute(command, timeout=1, as_root=as_root)
        except TimeoutError:
            pass
        else:
            raise ValueError(
                'Background command exited before timeout; got "{}"'.format(
                    output))

    def get_pids_of(self, process_name):
        """Returns a list of PIDs of all processes with the specified name."""
        result = (self.execute('ps | {} grep {}'.format(
            self.busybox, process_name),
                               check_exit_code=False) or '').strip()
        if result and 'not found' not in result:
            return [int(x.split()[1]) for x in result.split('\n')]
        else:
            return []

    def ps(self, **kwargs):
        """
        Returns the list of running processes on the device. Keyword arguments may
        be used to specify simple filters for columns.

        Added in version 2.1.4

        """
        lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
        lines.next()  # header
        result = []
        for line in lines:
            parts = line.split()
            if parts:
                result.append(
                    PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
        if not kwargs:
            return result
        else:
            filtered_result = []
            for entry in result:
                if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
                    filtered_result.append(entry)
            return filtered_result

    def get_properties(self, context):
        """Captures and saves the information from /system/build.prop and /proc/version"""
        props = super(AndroidDevice, self).get_properties(context)
        props.update(self._get_android_properties(context))
        return props

    def _get_android_properties(self, context):
        props = {}
        props['android_id'] = self.get_android_id()
        self._update_build_properties(props)

        dumpsys_host_file = os.path.join(context.host_working_directory,
                                         'window.dumpsys')
        with open(dumpsys_host_file, 'w') as wfh:
            wfh.write(self.execute('dumpsys window'))
            context.add_run_artifact('dumpsys_window', dumpsys_host_file,
                                     'meta')

        prop_file = os.path.join(context.host_working_directory,
                                 'android-props.json')
        with open(prop_file, 'w') as wfh:
            json.dump(props, wfh)
        context.add_run_artifact('android_properties', prop_file, 'export')
        return props

    def getprop(self, prop=None):
        """Returns parsed output of Android getprop command. If a property is
        specified, only the value for that property will be returned (with
        ``None`` returned if the property doesn't exist. Otherwise,
        ``wlauto.utils.android.AndroidProperties`` will be returned, which is
        a dict-like object."""
        props = AndroidProperties(self.execute('getprop'))
        if prop:
            return props[prop]
        return props

    def deploy_sqlite3(self, context):
        host_file = context.resolver.get(
            Executable(NO_ONE, self.abi, 'sqlite3'))
        target_file = self.install_if_needed(host_file)
        return target_file

    # Android-specific methods. These either rely on specifics of adb or other
    # Android-only concepts in their interface and/or implementation.

    def forward_port(self, from_port, to_port):
        """
        Forward a port on the device to a port on localhost.

        :param from_port: Port on the device which to forward.
        :param to_port: Port on the localhost to which the device port will be forwarded.

        Ports should be specified using adb spec. See the "adb forward" section in "adb help".

        """
        adb_command(self.adb_name,
                    'forward {} {}'.format(from_port, to_port),
                    timeout=self.default_timeout)

    def dump_logcat(self, outfile, filter_spec=None):
        """
        Dump the contents of logcat, for the specified filter spec to the
        specified output file.
        See http://developer.android.com/tools/help/logcat.html

        :param outfile: Output file on the host into which the contents of the
                        log will be written.
        :param filter_spec: Logcat filter specification.
                            see http://developer.android.com/tools/debugging/debugging-log.html#filteringOutput

        """
        if self._logcat_poller:
            return self._logcat_poller.write_log(outfile)
        else:
            if filter_spec:
                command = 'logcat -d -s {} > {}'.format(filter_spec, outfile)
            else:
                command = 'logcat -d > {}'.format(outfile)
            return adb_command(self.adb_name,
                               command,
                               timeout=self.default_timeout)

    def clear_logcat(self):
        """Clear (flush) logcat log."""
        if self._logcat_poller:
            return self._logcat_poller.clear_buffer()
        else:
            return adb_shell(self.adb_name,
                             'logcat -c',
                             timeout=self.default_timeout)

    def get_screen_size(self):
        output = self.execute('dumpsys window')
        match = SCREEN_SIZE_REGEX.search(output)
        if match:
            return (int(match.group('width')), int(match.group('height')))
        else:
            return (0, 0)

    def perform_unlock_swipe(self):
        width, height = self.get_screen_size()
        command = 'input swipe {} {} {} {}'
        if self.swipe_to_unlock == "horizontal":
            swipe_heigh = height * 2 // 3
            start = 100
            stop = width - start
            self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
        if self.swipe_to_unlock == "vertical":
            swipe_middle = height / 2
            swipe_heigh = height * 2 // 3
            self.execute(
                command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
        else:  # Should never reach here
            raise DeviceError("Invalid swipe direction: {}".format(
                self.swipe_to_unlock))

    def capture_screen(self, filepath):
        """Caputers the current device screen into the specified file in a PNG format."""
        on_device_file = self.path.join(self.working_directory,
                                        'screen_capture.png')
        self.execute('screencap -p  {}'.format(on_device_file))
        self.pull_file(on_device_file, filepath)
        self.delete_file(on_device_file)

    def capture_ui_hierarchy(self, filepath):
        """Captures the current view hierarchy into the specified file in a XML format."""
        on_device_file = self.path.join(self.working_directory,
                                        'screen_capture.xml')
        self.execute('uiautomator dump {}'.format(on_device_file))
        self.pull_file(on_device_file, filepath)
        self.delete_file(on_device_file)

        parsed_xml = xml.dom.minidom.parse(filepath)
        with open(filepath, 'w') as f:
            f.write(parsed_xml.toprettyxml())

    def is_screen_on(self):
        """Returns ``True`` if the device screen is currently on, ``False`` otherwise."""
        output = self.execute('dumpsys power')
        match = SCREEN_STATE_REGEX.search(output)
        if match:
            return boolean(match.group(1))
        else:
            raise DeviceError('Could not establish screen state.')

    def ensure_screen_is_on(self):
        if not self.is_screen_on():
            self.execute('input keyevent 26')
            if self.swipe_to_unlock:
                self.perform_unlock_swipe()

    def disable_screen_lock(self):
        """
        Attempts to disable he screen lock on the device.

        .. note:: This does not always work...

        Added inversion 2.1.4

        """
        lockdb = '/data/system/locksettings.db'
        sqlcommand = "update locksettings set value='0' where name='screenlock.disabled';"
        f = tempfile.NamedTemporaryFile()
        try:
            f.write('{} {} "{}"'.format(self.sqlite, lockdb, sqlcommand))
            f.flush()
            on_device_executable = self.install_executable(
                f.name, with_name="disable_screen_lock")
        finally:
            f.close()
        self.execute(on_device_executable, as_root=True)

    def disable_selinux(self):
        # This may be invoked from intialize() so we can't use execute() or the
        # standard API for doing this.
        api_level = int(
            adb_shell(self.adb_name,
                      'getprop ro.build.version.sdk',
                      timeout=self.default_timeout).strip())
        # SELinux was added in Android 4.3 (API level 18). Trying to
        # 'getenforce' in earlier versions will produce an error.
        if api_level >= 18:
            se_status = self.execute('getenforce', as_root=True).strip()
            if se_status == 'Enforcing':
                self.execute('setenforce 0', as_root=True)

    def get_device_model(self):
        try:
            return self.getprop(prop='ro.product.device')
        except KeyError:
            return None

    def refresh_device_files(self, file_list):
        """
        Depending on the devices android version and root status, determine the
        appropriate method of forcing a re-index of the mediaserver cache for a given
        list of files.
        """
        if self.device.is_rooted or self.device.get_sdk_version(
        ) < 24:  # MM and below
            common_path = commonprefix(file_list, sep=self.device.path.sep)
            self.broadcast_media_mounted(common_path, self.device.is_rooted)
        else:
            for f in file_list:
                self.broadcast_media_scan_file(f)

    def broadcast_media_scan_file(self, filepath):
        """
        Force a re-index of the mediaserver cache for the specified file.
        """
        command = 'am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://'
        self.execute(command + filepath)

    def broadcast_media_mounted(self, dirpath, as_root=False):
        """
        Force a re-index of the mediaserver cache for the specified directory.
        """
        command = 'am broadcast -a  android.intent.action.MEDIA_MOUNTED -d file://'
        self.execute(command + dirpath, as_root=as_root)

    # Internal methods: do not use outside of the class.
    def _update_build_properties(self, props):
        try:
            regex = re.compile(r'\[([^\]]+)\]\s*:\s*\[([^\]]+)\]')
            for match in regex.finditer(self.execute("getprop")):
                key = match.group(1).strip()
                value = match.group(2).strip()
                props[key] = value
        except ValueError:
            self.logger.warning('Could not parse build.prop.')

    def _update_versions(self, filepath, props):
        with open(filepath) as fh:
            text = fh.read()
            props['version'] = text
            text = re.sub(r'#.*', '', text).strip()
            match = re.search(r'^(Linux version .*?)\s*\((gcc version .*)\)$',
                              text)
            if match:
                props['linux_version'] = match.group(1).strip()
                props['gcc_version'] = match.group(2).strip()
            else:
                self.logger.warning('Could not parse version string.')

    def _ensure_binaries_directory_is_writable(self):
        matched = []
        for entry in self.list_file_systems():
            if self.binaries_directory.rstrip('/').startswith(
                    entry.mount_point):
                matched.append(entry)
        if matched:
            entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
            if 'rw' not in entry.options:
                self.execute('mount -o rw,remount {} {}'.format(
                    entry.device, entry.mount_point),
                             as_root=True)
        else:
            raise DeviceError(
                'Could not find mount point for binaries directory {}'.format(
                    self.binaries_directory))