| # Copyright 2015 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import glob |
| import logging |
| import os |
| import pipes |
| import shutil |
| import socket |
| import sys |
| import tempfile |
| import time |
| |
| from autotest_lib.client.bin import test, utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import chrome, arc_common |
| |
| _ADB_KEYS_PATH = '/tmp/adb_keys' |
| _ADB_VENDOR_KEYS = 'ADB_VENDOR_KEYS' |
| _ANDROID_CONTAINER_PID_PATH = '/run/containers/android*/container.pid' |
| _ANDROID_DATA_ROOT_PATH = '/opt/google/containers/android/rootfs/android-data' |
| _ANDROID_CONTAINER_ROOT_PATH = '/opt/google/containers/android/rootfs' |
| _SCREENSHOT_DIR_PATH = '/var/log/arc-screenshots' |
| _SCREENSHOT_BASENAME = 'arc-screenshot' |
| _MAX_SCREENSHOT_NUM = 10 |
| # This address should match the one present in |
| # https://chromium.googlesource.com/chromiumos/overlays/chromiumos-overlay/+/master/chromeos-base/arc-sslh-init/files/sslh.conf |
| _ADBD_ADDRESS = ('100.115.92.2', 5555) |
| _ADBD_PID_PATH = '/run/arc/adbd.pid' |
| _SDCARD_PID_PATH = '/run/arc/sdcard.pid' |
| _ANDROID_ADB_KEYS_PATH = '/data/misc/adb/adb_keys' |
| _PROCESS_CHECK_INTERVAL_SECONDS = 1 |
| _PROPERTY_CHECK_INTERVAL_SECONDS = 1 |
| _WAIT_FOR_ADB_READY = 60 |
| _WAIT_FOR_ANDROID_PROCESS_SECONDS = 60 |
| _PLAY_STORE_PKG = 'com.android.vending' |
| _SETTINGS_PKG = 'com.android.settings' |
| |
| |
| def setup_adb_host(): |
| """Setup ADB host keys. |
| |
| This sets up the files and environment variables that wait_for_adb_ready() |
| needs""" |
| if _ADB_VENDOR_KEYS in os.environ: |
| return |
| if not os.path.exists(_ADB_KEYS_PATH): |
| os.mkdir(_ADB_KEYS_PATH) |
| # adb expects $HOME to be writable. |
| os.environ['HOME'] = _ADB_KEYS_PATH |
| |
| # Generate and save keys for adb if needed |
| key_path = os.path.join(_ADB_KEYS_PATH, 'test_key') |
| if not os.path.exists(key_path): |
| utils.system('adb keygen ' + pipes.quote(key_path)) |
| os.environ[_ADB_VENDOR_KEYS] = key_path |
| |
| |
| def restart_adbd(timeout): |
| """Restarts the adb daemon. |
| |
| Follows the same logic as tast. |
| """ |
| logging.debug('restarting adbd') |
| config = 'adb' |
| _android_shell('setprop persist.sys.usb.config ' + config) |
| _android_shell('setprop sys.usb.config ' + config) |
| |
| def property_check(): |
| return _android_shell('getprop sys.usb.state') == config |
| |
| try: |
| utils.poll_for_condition( |
| condition=property_check, |
| desc='Wait for sys.usb.state', |
| timeout=timeout, |
| sleep_interval=_PROPERTY_CHECK_INTERVAL_SECONDS) |
| except utils.TimeoutError: |
| raise error.TestFail('Timed out waiting for sys.usb.state change') |
| |
| _android_shell('setprop ctl.restart adbd') |
| |
| |
| def restart_adb(): |
| """Restarts adb. |
| |
| Follows the same logic as in tast, specifically avoiding kill-server |
| since it is unreliable (crbug.com/855325). |
| """ |
| logging.debug('killing and restarting adb server') |
| utils.system('killall --quiet --wait -KILL adb') |
| utils.system('adb start-server') |
| |
| |
| def is_adb_connected(): |
| """Return true if adb is connected to the container.""" |
| output = utils.system_output('adb get-state', ignore_status=True) |
| logging.debug('adb get-state: %s', output) |
| return output.strip() == 'device' |
| |
| |
| def _is_android_data_mounted(): |
| """Return true if Android's /data is mounted with partial boot enabled.""" |
| return _android_shell('getprop ro.data_mounted', ignore_status=True) == '1' |
| |
| |
| def get_zygote_type(): |
| """Return zygote service type.""" |
| return _android_shell('getprop ro.zygote', ignore_status=True) |
| |
| |
| def get_sdk_version(): |
| """Return the SDK level version for Android.""" |
| return _android_shell('getprop ro.build.version.sdk') |
| |
| |
| def get_product(): |
| """Return the product string used for the Android build.""" |
| return _android_shell('getprop ro.build.product', ignore_status=True) |
| |
| |
| def _is_tcp_port_reachable(address): |
| """Return whether a TCP port described by |address| is reachable.""" |
| try: |
| s = socket.create_connection(address) |
| s.close() |
| return True |
| except socket.error: |
| return False |
| |
| |
| def _wait_for_data_mounted(timeout): |
| utils.poll_for_condition( |
| condition=_is_android_data_mounted, |
| desc='Wait for /data mounted', |
| timeout=timeout, |
| sleep_interval=_PROCESS_CHECK_INTERVAL_SECONDS) |
| |
| |
| def wait_for_adb_ready(timeout=_WAIT_FOR_ADB_READY): |
| """Wait for the ADB client to connect to the ARC container. |
| |
| @param timeout: Timeout in seconds. |
| """ |
| # Although adbd is started at login screen, we still need /data to be |
| # mounted to set up key-based authentication. /data should be mounted |
| # once the user has logged in. |
| |
| initial_timeout = timeout |
| |
| start_time = time.time() |
| _wait_for_data_mounted(timeout) |
| timeout -= (time.time() - start_time) |
| start_time = time.time() |
| arc_common.wait_for_android_boot(timeout) |
| timeout -= (time.time() - start_time) |
| |
| setup_adb_host() |
| if is_adb_connected(): |
| return |
| |
| # Push keys for adb. |
| pubkey_path = os.environ[_ADB_VENDOR_KEYS] + '.pub' |
| with open(pubkey_path, 'r') as f: |
| _write_android_file(_ANDROID_ADB_KEYS_PATH, f.read()) |
| _android_shell('chown shell ' + pipes.quote(_ANDROID_ADB_KEYS_PATH)) |
| _android_shell('restorecon ' + pipes.quote(_ANDROID_ADB_KEYS_PATH)) |
| |
| attempt_count = 3 |
| timeout = timeout / attempt_count |
| |
| for i in range(attempt_count): |
| if _restart_adb_and_wait_for_ready(timeout): |
| return |
| raise error.TestFail( |
| 'Failed to connect to adb in %d seconds.' % initial_timeout) |
| |
| |
| def _restart_adb_and_wait_for_ready(timeout): |
| """Restart adb/adbd and wait adb connection is ready. |
| |
| @param timeout: Timeout in seconds. |
| @return True in case adb connection was established or throw an error in |
| case persistent error occured. |
| """ |
| |
| # Restart adbd and adb. |
| start_time = time.time() |
| restart_adbd(timeout) |
| timeout -= (time.time() - start_time) |
| start_time = time.time() |
| restart_adb() |
| timeout -= (time.time() - start_time) |
| |
| try: |
| utils.poll_for_condition(condition=is_adb_connected, |
| timeout=timeout) |
| return True |
| except (utils.TimeoutError): |
| # The operation has failed, but let's try to clarify the failure to |
| # avoid shifting blame to adb. |
| |
| # First, collect some information and log it. |
| arc_alive = is_android_container_alive() |
| arc_booted = _android_shell('getprop sys.boot_completed', |
| ignore_status=True) |
| arc_system_events = _android_shell( |
| 'logcat -d -b events *:S arc_system_event', ignore_status=True) |
| adbd_pid = _android_shell('pidof -s adbd', ignore_status=True) |
| adbd_port_reachable = _is_tcp_port_reachable(_ADBD_ADDRESS) |
| adb_state = utils.system_output('adb get-state', ignore_status=True) |
| logging.debug('ARC alive: %s', arc_alive) |
| logging.debug('ARC booted: %s', arc_booted) |
| logging.debug('ARC system events: %s', arc_system_events) |
| logging.debug('adbd process: %s', adbd_pid) |
| logging.debug('adbd port reachable: %s', adbd_port_reachable) |
| logging.debug('adb state: %s', adb_state) |
| |
| # Now go through the usual suspects and raise nicer errors to make the |
| # actual failure clearer. |
| if not arc_alive: |
| raise error.TestFail('ARC is not alive.') |
| if arc_booted != '1': |
| raise error.TestFail('ARC did not finish booting.') |
| return False |
| |
| |
| def grant_permissions(package, permissions): |
| """Grants permissions to a package. |
| |
| @param package: Package name. |
| @param permissions: A list of permissions. |
| |
| """ |
| for permission in permissions: |
| adb_shell('pm grant %s android.permission.%s' % ( |
| pipes.quote(package), pipes.quote(permission))) |
| |
| |
| def adb_cmd(cmd, **kwargs): |
| """Executed cmd using adb. Must wait for adb ready. |
| |
| @param cmd: Command to run. |
| """ |
| # TODO(b/79122489) - Assert if cmd == 'root' |
| wait_for_adb_ready() |
| return utils.system_output('adb %s' % cmd, **kwargs) |
| |
| |
| def adb_shell(cmd, **kwargs): |
| """Executed shell command with adb. |
| |
| @param cmd: Command to run. |
| """ |
| output = adb_cmd('shell %s' % pipes.quote(cmd), **kwargs) |
| # Some android commands include a trailing CRLF in their output. |
| if kwargs.pop('strip_trailing_whitespace', True): |
| output = output.rstrip() |
| return output |
| |
| |
| def adb_install(apk, auto_grant_permissions=True, ignore_status=False): |
| """Install an apk into container. You must connect first. |
| |
| @param apk: Package to install. |
| @param auto_grant_permissions: Set to false to not automatically grant all |
| permissions. Most tests should not care. |
| @param ignore_status: Set to true to allow the install command to fail, |
| for example if you are installing multiple architectures and only need |
| one to succeed. |
| """ |
| flags = '-g' if auto_grant_permissions else '' |
| return adb_cmd('install -r -t %s %s' % (flags, apk), |
| timeout=60*5, |
| ignore_status=ignore_status) |
| |
| |
| def adb_uninstall(apk): |
| """Remove an apk from container. You must connect first. |
| |
| @param apk: Package to uninstall. |
| """ |
| return adb_cmd('uninstall %s' % apk) |
| |
| |
| def adb_reboot(): |
| """Reboots the container and block until container pid is gone. |
| |
| You must connect first. |
| """ |
| old_pid = get_container_pid() |
| logging.info('Trying to reboot PID:%s', old_pid) |
| adb_cmd('reboot', ignore_status=True) |
| # Ensure that the old container is no longer booted |
| utils.poll_for_condition( |
| lambda: not utils.pid_is_alive(int(old_pid)), timeout=10) |
| |
| |
| # This adb_root() function is deceiving in that it works just fine on debug |
| # builds of ARC (user-debug, eng). However "adb root" does not work on user |
| # builds as run by the autotest machines when testing prerelease images. In fact |
| # it will silently fail. You will need to find another way to do do what you |
| # need to do as root. |
| # |
| # TODO(b/79122489) - Remove this function. |
| def adb_root(): |
| """Restart adbd with root permission.""" |
| |
| adb_cmd('root') |
| |
| |
| def get_container_root(): |
| """Returns path to Android container root directory.""" |
| return _ANDROID_CONTAINER_ROOT_PATH |
| |
| |
| def get_container_pid_path(): |
| """Returns the container's PID file path. |
| |
| Raises: |
| TestError if no PID file is found, or more than one files are found. |
| """ |
| # Find the PID file rather than the android-XXXXXX/ directory to ignore |
| # stale and empty android-XXXXXX/ directories when they exist. |
| arc_container_pid_files = glob.glob(_ANDROID_CONTAINER_PID_PATH) |
| |
| if len(arc_container_pid_files) == 0: |
| raise error.TestError('Android container.pid not available') |
| |
| if len(arc_container_pid_files) > 1: |
| raise error.TestError( |
| 'Multiple Android container.pid files found: %r. ' |
| 'Reboot your DUT to recover.' % (arc_container_pid_files)) |
| |
| return arc_container_pid_files[0] |
| |
| |
| def get_android_data_root(): |
| """Returns path to Chrome OS directory that bind-mounts Android's /data.""" |
| return _ANDROID_DATA_ROOT_PATH |
| |
| |
| def get_container_pid(): |
| """Returns the PID of the container.""" |
| return utils.read_one_line(get_container_pid_path()) |
| |
| |
| def get_adbd_pid(): |
| """Returns the PID of the adbd proxy container.""" |
| if not os.path.exists(_ADBD_PID_PATH): |
| # The adbd proxy does not run on all boards. |
| return None |
| return utils.read_one_line(_ADBD_PID_PATH) |
| |
| |
| def is_android_process_running(process_name): |
| """Return whether Android has completed booting. |
| |
| @param process_name: Process name. |
| """ |
| output = adb_shell('pgrep -c -f %s' % pipes.quote(process_name), |
| ignore_status=True) |
| return int(output) > 0 |
| |
| |
| def check_android_file_exists(filename): |
| """Checks whether the given file exists in the Android filesystem |
| |
| @param filename: File to check. |
| """ |
| return adb_shell( |
| 'test -e {} && echo FileExists'.format(pipes.quote(filename)), |
| ignore_status=True).find("FileExists") >= 0 |
| |
| |
| def read_android_file(filename): |
| """Reads a file in Android filesystem. |
| |
| @param filename: File to read. |
| """ |
| with tempfile.NamedTemporaryFile() as tmpfile: |
| adb_cmd('pull %s %s' % (pipes.quote(filename), |
| pipes.quote(tmpfile.name))) |
| with open(tmpfile.name) as f: |
| return f.read() |
| |
| return None |
| |
| |
| def write_android_file(filename, data): |
| """Writes to a file in Android filesystem. |
| |
| @param filename: File to write. |
| @param data: Data to write. |
| """ |
| with tempfile.NamedTemporaryFile() as tmpfile: |
| tmpfile.write(data) |
| tmpfile.flush() |
| |
| adb_cmd('push %s %s' % (pipes.quote(tmpfile.name), |
| pipes.quote(filename))) |
| |
| |
| def _write_android_file(filename, data): |
| """Writes to a file in Android filesystem. |
| |
| This is an internal function used to bootstrap adb. |
| Tests should use write_android_file instead. |
| """ |
| android_cmd = 'cat > %s' % pipes.quote(filename) |
| cros_cmd = 'android-sh -c %s' % pipes.quote(android_cmd) |
| utils.run(cros_cmd, stdin=data) |
| |
| |
| def get_android_file_stats(filename): |
| """Returns an object of file stats for an Android file. |
| |
| The returned object supported limited attributes, but can be easily extended |
| if needed. Note that the value are all string. |
| |
| This uses _android_shell to run as root, so that it can access to all files |
| inside the container. On non-debuggable build, adb shell is not rootable. |
| """ |
| mapping = { |
| '%a': 'mode', |
| '%g': 'gid', |
| '%h': 'nlink', |
| '%u': 'uid', |
| } |
| output = _android_shell( |
| 'stat -c "%s" %s' % (' '.join(mapping.keys()), pipes.quote(filename)), |
| ignore_status=True) |
| stats = output.split(' ') |
| if len(stats) != len(mapping): |
| raise error.TestError('Unexpected output from stat: %s' % output) |
| _Stats = collections.namedtuple('_Stats', mapping.values()) |
| return _Stats(*stats) |
| |
| |
| def remove_android_file(filename): |
| """Removes a file in Android filesystem. |
| |
| @param filename: File to remove. |
| """ |
| adb_shell('rm -f %s' % pipes.quote(filename)) |
| |
| |
| def wait_for_android_boot(timeout=None): |
| """Sleep until Android has completed booting or timeout occurs. |
| |
| @param timeout: Timeout in seconds. |
| """ |
| arc_common.wait_for_android_boot(timeout) |
| |
| |
| def wait_for_android_process(process_name, |
| timeout=_WAIT_FOR_ANDROID_PROCESS_SECONDS): |
| """Sleep until an Android process is running or timeout occurs. |
| |
| @param process_name: Process name. |
| @param timeout: Timeout in seconds. |
| """ |
| condition = lambda: is_android_process_running(process_name) |
| utils.poll_for_condition(condition=condition, |
| desc='%s is running' % process_name, |
| timeout=timeout, |
| sleep_interval=_PROCESS_CHECK_INTERVAL_SECONDS) |
| |
| |
| def _android_shell(cmd, **kwargs): |
| """Execute cmd instead the Android container. |
| |
| This function is strictly for internal use only, as commands do not run in |
| a fully consistent Android environment. Prefer adb_shell instead. |
| """ |
| return utils.system_output('android-sh -c {}'.format(pipes.quote(cmd)), |
| **kwargs) |
| |
| |
| def is_android_container_alive(): |
| """Check if android container is alive.""" |
| try: |
| container_pid = get_container_pid() |
| except Exception, e: |
| logging.error('is_android_container_alive failed: %r', e) |
| return False |
| return utils.pid_is_alive(int(container_pid)) |
| |
| |
| def _is_in_installed_packages_list(package, option=None): |
| """Check if a package is in the list returned by pm list packages. |
| |
| adb must be ready. |
| |
| @param package: Package in request. |
| @param option: An option for the command adb shell pm list packages. |
| Valid values include '-s', '-3', '-d', and '-e'. |
| """ |
| command = 'pm list packages' |
| if option: |
| command += ' ' + option |
| packages = adb_shell(command).splitlines() |
| package_entry = 'package:' + package |
| ret = package_entry in packages |
| |
| if not ret: |
| logging.info('Could not find "%s" in %s', |
| package_entry, str(packages)) |
| return ret |
| |
| |
| def is_package_installed(package): |
| """Check if a package is installed. adb must be ready. |
| |
| @param package: Package in request. |
| """ |
| return _is_in_installed_packages_list(package) |
| |
| |
| def is_package_disabled(package): |
| """Check if an installed package is disabled. adb must be ready. |
| |
| @param package: Package in request. |
| """ |
| return _is_in_installed_packages_list(package, '-d') |
| |
| |
| def get_package_install_path(package): |
| """Returns the apk install location of the given package.""" |
| output = adb_shell('pm path {}'.format(pipes.quote(package))) |
| return output.split(':')[1] |
| |
| |
| def _before_iteration_hook(obj): |
| """Executed by parent class before every iteration. |
| |
| This function resets the run_once_finished flag before every iteration |
| so we can detect failure on every single iteration. |
| |
| Args: |
| obj: the test itself |
| """ |
| obj.run_once_finished = False |
| |
| |
| def _after_iteration_hook(obj): |
| """Executed by parent class after every iteration. |
| |
| The parent class will handle exceptions and failures in the run and will |
| always call this hook afterwards. Take a screenshot if the run has not |
| been marked as finished (i.e. there was a failure/exception). |
| |
| Args: |
| obj: the test itself |
| """ |
| if not obj.run_once_finished: |
| if is_adb_connected(): |
| logging.debug('Recent activities dump:\n%s', |
| adb_shell('dumpsys activity recents', |
| ignore_status=True)) |
| if not os.path.exists(_SCREENSHOT_DIR_PATH): |
| os.mkdir(_SCREENSHOT_DIR_PATH, 0755) |
| obj.num_screenshots += 1 |
| if obj.num_screenshots <= _MAX_SCREENSHOT_NUM: |
| logging.warning('Iteration %d failed, taking a screenshot.', |
| obj.iteration) |
| try: |
| utils.run('screenshot "{}/{}_iter{}.png"'.format( |
| _SCREENSHOT_DIR_PATH, _SCREENSHOT_BASENAME, obj.iteration)) |
| except Exception as e: |
| logging.warning('Unable to capture screenshot. %s', e) |
| else: |
| logging.warning('Too many failures, no screenshot taken') |
| |
| |
| def send_keycode(keycode): |
| """Sends the given keycode to the container |
| |
| @param keycode: keycode to send. |
| """ |
| adb_shell('input keyevent {}'.format(keycode)) |
| |
| |
| def get_android_sdk_version(): |
| """Returns the Android SDK version. |
| |
| This function can be called before Android container boots. |
| """ |
| with open('/etc/lsb-release') as f: |
| values = dict(line.split('=', 1) for line in f.read().splitlines()) |
| try: |
| return int(values['CHROMEOS_ARC_ANDROID_SDK_VERSION']) |
| except (KeyError, ValueError): |
| raise error.TestError('Could not determine Android SDK version') |
| |
| |
| def set_device_mode(device_mode, use_fake_sensor_with_lifetime_secs=0): |
| """Sets the device in either Clamshell or Tablet mode. |
| |
| "inject_powerd_input_event" might fail if the DUT does not support Tablet |
| mode, and it will raise an |error.CmdError| exception. To prevent that, use |
| the |use_fake_sensor_with_lifetime_secs| parameter. |
| |
| @param device_mode: string with either 'clamshell' or 'tablet' |
| @param use_fake_sensor_with_lifetime_secs: if > 0, it will create the |
| input device with the given lifetime in seconds |
| @raise ValueError: if passed invalid parameters |
| @raise error.CmdError: if inject_powerd_input_event fails |
| """ |
| valid_value = ('tablet', 'clamshell') |
| if device_mode not in valid_value: |
| raise ValueError('Invalid device_mode parameter: %s' % device_mode) |
| |
| value = 1 if device_mode == 'tablet' else 0 |
| |
| args = ['--code=tablet', '--value=%d' % value] |
| |
| if use_fake_sensor_with_lifetime_secs > 0: |
| args.extend(['--create_dev', '--dev_lifetime=%d' % |
| use_fake_sensor_with_lifetime_secs]) |
| |
| try: |
| utils.run('inject_powerd_input_event', args=args) |
| except error.CmdError as err: |
| # TODO: Fragile code ahead. Correct way to do it is to check |
| # if device is already in desired mode, and do nothing if so. |
| # ATM we don't have a way to check current device mode. |
| |
| # Assuming that CmdError means that device does not support |
| # --code=tablet parameter, meaning that device only supports clamshell |
| # mode. |
| if device_mode == 'clamshell' and \ |
| use_fake_sensor_with_lifetime_secs == 0: |
| return |
| raise err |
| |
| |
| def wait_for_userspace_ready(): |
| """Waits for userspace apps to be launchable. |
| |
| Launches and then closes Android settings as a way to ensure all basic |
| services are ready. This goes a bit beyond waiting for boot-up to complete, |
| as being able to launch an activity requires more of the framework to have |
| started. The boot-complete signal happens fairly early, and the framework |
| system server is still starting services. By waiting for ActivityManager to |
| respond, we automatically wait on more services to be ready. |
| """ |
| output = adb_shell('am start -W -a android.settings.SETTINGS', |
| ignore_status=True) |
| if not output.endswith('Complete'): |
| logging.debug('Output was: %s', output) |
| raise error.TestError('Could not launch SETTINGS') |
| adb_shell('am force-stop com.android.settings', ignore_status=True) |
| |
| |
| class ArcTest(test.test): |
| """ Base class of ARC Test. |
| |
| This class could be used as super class of an ARC test for saving |
| redundant codes for container bringup, autotest-dep package(s) including |
| uiautomator setup if required, and apks install/remove during |
| arc_setup/arc_teardown, respectively. By default arc_setup() is called in |
| initialize() after Android have been brought up. It could also be |
| overridden to perform non-default tasks. For example, a simple |
| ArcHelloWorldTest can be just implemented with print 'HelloWorld' in its |
| run_once() and no other functions are required. We could expect |
| ArcHelloWorldTest would bring up browser and wait for container up, then |
| print 'Hello World', and shutdown browser after. As a precaution, if you |
| overwrite initialize(), arc_setup(), or cleanup() function(s) in ARC test, |
| remember to call the corresponding function(s) in this base class as well. |
| """ |
| version = 1 |
| _PKG_UIAUTOMATOR = 'uiautomator' |
| _FULL_PKG_NAME_UIAUTOMATOR = 'com.github.uiautomator' |
| |
| def __init__(self, *args, **kwargs): |
| """Initialize flag setting.""" |
| super(ArcTest, self).__init__(*args, **kwargs) |
| self.initialized = False |
| # Set the flag run_once_finished to detect if a test is executed |
| # successfully without any exception thrown. Otherwise, generate |
| # a screenshot in /var/log for debugging. |
| self.run_once_finished = False |
| self.logcat_proc = None |
| self.dep_package = None |
| self.apks = None |
| self.full_pkg_names = [] |
| self.uiautomator = False |
| self._should_reenable_play_store = False |
| self._chrome = None |
| if os.path.exists(_SCREENSHOT_DIR_PATH): |
| shutil.rmtree(_SCREENSHOT_DIR_PATH) |
| self.register_before_iteration_hook(_before_iteration_hook) |
| self.register_after_iteration_hook(_after_iteration_hook) |
| # Keep track of the number of debug screenshots taken and keep the |
| # total number sane to avoid issues. |
| self.num_screenshots = 0 |
| |
| def initialize(self, extension_path=None, username=None, password=None, |
| arc_mode=arc_common.ARC_MODE_ENABLED, **chrome_kargs): |
| """Log in to a test account.""" |
| extension_paths = [extension_path] if extension_path else [] |
| self._chrome = chrome.Chrome(extension_paths=extension_paths, |
| username=username, |
| password=password, |
| arc_mode=arc_mode, |
| **chrome_kargs) |
| if extension_path: |
| self._extension = self._chrome.get_extension(extension_path) |
| else: |
| self._extension = None |
| # With ARC enabled, Chrome will wait until container to boot up |
| # before returning here, see chrome.py. |
| self.initialized = True |
| try: |
| if is_android_container_alive(): |
| self.arc_setup() |
| else: |
| logging.error('Container is alive?') |
| except Exception as err: |
| raise error.TestFail(err) |
| |
| def after_run_once(self): |
| """Executed after run_once() only if there were no errors. |
| |
| This function marks the run as finished with a flag. If there was a |
| failure the flag won't be set and the failure can then be detected by |
| testing the run_once_finished flag. |
| """ |
| logging.info('After run_once') |
| self.run_once_finished = True |
| |
| def cleanup(self): |
| """Log out of Chrome.""" |
| if not self.initialized: |
| logging.info('Skipping ARC cleanup: not initialized') |
| return |
| logging.info('Starting ARC cleanup') |
| try: |
| if is_android_container_alive(): |
| self.arc_teardown() |
| except Exception as err: |
| raise error.TestFail(err) |
| finally: |
| try: |
| if self.logcat_proc: |
| self.logcat_proc.close() |
| finally: |
| if self._chrome is not None: |
| self._chrome.close() |
| |
| def _install_apks(self, dep_package, apks, full_pkg_names): |
| """"Install apks fetched from the specified package folder. |
| |
| @param dep_package: A dependent package directory |
| @param apks: List of apk names to be installed |
| @param full_pkg_names: List of packages to be uninstalled at teardown |
| """ |
| apk_path = os.path.join(self.autodir, 'deps', dep_package) |
| if apks: |
| for apk in apks: |
| logging.info('Installing %s', apk) |
| out = adb_install('%s/%s' % (apk_path, apk), ignore_status=True) |
| logging.info('Install apk output: %s', str(out)) |
| # Verify if package(s) are installed correctly. We ignored |
| # individual install statuses above because some tests list apks for |
| # all arches and only need one installed. |
| if not full_pkg_names: |
| raise error.TestError('Package names of apks expected') |
| for pkg in full_pkg_names: |
| logging.info('Check if %s is installed', pkg) |
| if not is_package_installed(pkg): |
| raise error.TestError('Package %s not found' % pkg) |
| # Make sure full_pkg_names contains installed packages only |
| # so arc_teardown() knows what packages to uninstall. |
| self.full_pkg_names.append(pkg) |
| |
| def _count_nested_array_level(self, array): |
| """Count the level of a nested array.""" |
| if isinstance(array, list): |
| return 1 + self._count_nested_array_level(array[0]) |
| return 0 |
| |
| def _fix_nested_array_level(self, var_name, expected_level, array): |
| """Enclose array one level deeper if needed.""" |
| level = self._count_nested_array_level(array) |
| if level == expected_level: |
| return array |
| if level == expected_level - 1: |
| return [array] |
| |
| logging.error("Variable %s nested level is not fixable: " |
| "Expecting %d, seeing %d", |
| var_name, expected_level, level) |
| raise error.TestError('Format error with variable %s' % var_name) |
| |
| def arc_setup(self, dep_packages=None, apks=None, full_pkg_names=None, |
| uiautomator=False, disable_play_store=False): |
| """ARC test setup: Setup dependencies and install apks. |
| |
| This function disables package verification and enables non-market |
| APK installation. Then, it installs specified APK(s) and uiautomator |
| package and path if required in a test. |
| |
| @param dep_packages: Array of package names of autotest_deps APK |
| packages. |
| @param apks: Array of APK name arrays to be installed in dep_package. |
| @param full_pkg_names: Array of full package name arrays to be removed |
| in teardown. |
| @param uiautomator: uiautomator python package is required or not. |
| @param disable_play_store: Set this to True if you want to prevent |
| GMS Core from updating. |
| """ |
| if not self.initialized: |
| logging.info('Skipping ARC setup: not initialized') |
| return |
| logging.info('Starting ARC setup') |
| |
| # Sample parameters for multi-deps setup after fixup (if needed): |
| # dep_packages: ['Dep1-apk', 'Dep2-apk'] |
| # apks: [['com.dep1.arch1.apk', 'com.dep2.arch2.apk'], ['com.dep2.apk'] |
| # full_pkg_nmes: [['com.dep1.app'], ['com.dep2.app']] |
| # TODO(crbug/777787): once the parameters of all callers of arc_setup |
| # are refactored, we can delete the safety net here. |
| if dep_packages: |
| dep_packages = self._fix_nested_array_level( |
| 'dep_packages', 1, dep_packages) |
| apks = self._fix_nested_array_level('apks', 2, apks) |
| full_pkg_names = self._fix_nested_array_level( |
| 'full_pkg_names', 2, full_pkg_names) |
| if (len(dep_packages) != len(apks) or |
| len(apks) != len(full_pkg_names)): |
| logging.info('dep_packages length is %d', len(dep_packages)) |
| logging.info('apks length is %d', len(apks)) |
| logging.info('full_pkg_names length is %d', |
| len(full_pkg_names)) |
| raise error.TestFail( |
| 'dep_packages/apks/full_pkg_names format error') |
| |
| self.dep_packages = dep_packages |
| self.apks = apks |
| self.uiautomator = uiautomator or disable_play_store |
| # Setup dependent packages if required |
| packages = [] |
| if dep_packages: |
| packages = dep_packages[:] |
| if self.uiautomator: |
| packages.append(self._PKG_UIAUTOMATOR) |
| if packages: |
| logging.info('Setting up dependent package(s) %s', packages) |
| self.job.setup_dep(packages) |
| |
| self.logcat_proc = arc_common.Logcat() |
| |
| wait_for_adb_ready() |
| |
| # Setting verifier_verify_adb_installs to zero suppresses a dialog box |
| # that can appear asking for the user to consent to the install. |
| adb_shell('settings put global verifier_verify_adb_installs 0') |
| |
| # Install apks based on dep_packages/apks/full_pkg_names tuples |
| if dep_packages: |
| for i in xrange(len(dep_packages)): |
| self._install_apks(dep_packages[i], apks[i], full_pkg_names[i]) |
| |
| if self.uiautomator: |
| path = os.path.join(self.autodir, 'deps', self._PKG_UIAUTOMATOR) |
| sys.path.append(path) |
| self._add_ui_object_not_found_handler() |
| if disable_play_store and not is_package_disabled(_PLAY_STORE_PKG): |
| self._disable_play_store() |
| if not is_package_disabled(_PLAY_STORE_PKG): |
| raise error.TestFail('Failed to disable Google Play Store.') |
| self._should_reenable_play_store = True |
| |
| def arc_teardown(self): |
| """ARC test teardown. |
| |
| This function removes all installed packages in arc_setup stage |
| first. Then, it restores package verification and disables non-market |
| APK installation. |
| |
| """ |
| if self.full_pkg_names: |
| for pkg in self.full_pkg_names: |
| logging.info('Uninstalling %s', pkg) |
| if not is_package_installed(pkg): |
| raise error.TestError('Package %s was not installed' % pkg) |
| adb_uninstall(pkg) |
| if (self.uiautomator and |
| is_package_installed(self._FULL_PKG_NAME_UIAUTOMATOR)): |
| logging.info('Uninstalling %s', self._FULL_PKG_NAME_UIAUTOMATOR) |
| adb_uninstall(self._FULL_PKG_NAME_UIAUTOMATOR) |
| if self._should_reenable_play_store: |
| adb_shell('pm enable ' + _PLAY_STORE_PKG) |
| adb_shell('settings put secure install_non_market_apps 0') |
| adb_shell('settings put global package_verifier_enable 1') |
| adb_shell('settings put secure package_verifier_user_consent 0') |
| |
| # Remove the adb keys without going through adb. This is because the |
| # 'rm' tool does not have permissions to remove the keys once they have |
| # been restorecon(8)ed. |
| utils.system_output('rm -f %s' % |
| pipes.quote(os.path.join( |
| get_android_data_root(), |
| os.path.relpath(_ANDROID_ADB_KEYS_PATH, '/')))) |
| utils.system_output('adb kill-server') |
| |
| def _add_ui_object_not_found_handler(self): |
| """Logs the device dump upon uiautomator.UiObjectNotFoundException.""" |
| from uiautomator import device as d |
| d.handlers.on(lambda d: logging.debug('Device window dump:\n%s', |
| d.dump())) |
| |
| def _disable_play_store(self): |
| """Disables the Google Play Store app.""" |
| if is_package_disabled(_PLAY_STORE_PKG): |
| return |
| adb_shell('am force-stop ' + _PLAY_STORE_PKG) |
| adb_shell('am start -a android.settings.APPLICATION_DETAILS_SETTINGS ' |
| '-d package:' + _PLAY_STORE_PKG) |
| |
| # Note: the straightforward "pm disable <package>" command would be |
| # better, but that requires root permissions, which aren't available on |
| # a pre-release image being tested. The only other way is through the |
| # Settings UI, but which might change. |
| from uiautomator import device as d |
| d(textMatches='(?i)DISABLE', packageName=_SETTINGS_PKG).wait.exists() |
| d(textMatches='(?i)DISABLE', packageName=_SETTINGS_PKG).click.wait() |
| d(textMatches='(?i)DISABLE APP').click.wait() |
| ok_button = d(textMatches='(?i)OK') |
| if ok_button.exists: |
| ok_button.click.wait() |
| adb_shell('am force-stop ' + _SETTINGS_PKG) |