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()
class MyOverridingExtension(MyAcidExtension): name = 'overriding' parameters = [ Parameter('hydrochloric', override=True, default=[3, 4]), ]
class MultiValueParamExt(Extension): name = 'multivalue' parameters = [ Parameter('test', kind=list_of_ints, allowed_values=[42, 7, 73]), ]
class MyModularExtension(Extension): name = 'modular' parameters = [ Parameter('modules', override=True, default=['cool_module']), ]
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
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
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
class BigLittleDevice(AndroidDevice): # pylint: disable=W0223 parameters = [ Parameter('scheduler', default='hmp', override=True), ]
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))
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)
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)
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
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]
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)
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))
class DuplicateParamExtension(MyBaseExtension): # pylint: disable=W0612 parameters = [ Parameter('food', override=True, default='cheese'), ]
class DuplicateParamExtension(MyBaseExtension): # pylint: disable=W0612 parameters = [ Parameter('base', override=True, default='buttery'), Parameter('base', override=True, default='biscuit'), ]
class OverridingExtension(MyBaseExtension): # pylint: disable=W0612 parameters = [ Parameter('base', override=True, default='cheese'), ]
class BadExtension(MyBaseExtension): # pylint: disable=W0612 parameters = [ Parameter('base'), ]
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)
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
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))
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))