blob: e1f7a8bc1b87e623f8f321d2f031cd4e6f4ba922 [file] [log] [blame]
# 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 re
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 adb_connect():
"""Attempt to connect ADB to the Android container.
Returns true if successful. Do not call this function directly. Call
wait_for_adb_ready() instead.
"""
if utils.system('adb connect localhost:22', ignore_status=True) != 0:
return False
return is_adb_connected()
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=adb_connect,
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_job_pid(job_name):
"""Returns the PID of an upstart job."""
status = utils.system_output('status %s' % job_name)
match = re.match(r'^%s start/running, process (\d+)$' % job_name,
status)
if not match:
raise error.TestError('Unexpected status: "%s"' % status)
return match.group(1)
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 get_sdcard_pid():
"""Returns the PID of the sdcard container."""
return utils.read_one_line(_SDCARD_PID_PATH)
def get_mount_passthrough_pid_list():
"""Returns PIDs of ARC mount-passthrough daemon jobs."""
JOB_NAMES = [ 'arc-myfiles', 'arc-myfiles-default',
'arc-myfiles-read', 'arc-myfiles-write',
'arc-removable-media', 'arc-removable-media-default',
'arc-removable-media-read', 'arc-removable-media-write' ]
pid_list = []
for job_name in JOB_NAMES:
try:
pid = get_job_pid(job_name)
pid_list.append(pid)
except Exception, e:
logging.warning('Failed to find PID for %s : %s', job_name, e)
continue
return pid_list
def get_obb_mounter_pid():
"""Returns the PID of the OBB mounter."""
return utils.system_output('pgrep -f -u root ^/usr/bin/arc-obb-mounter')
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, block_outbound=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 block_outbound: block outbound network traffic during a test.
@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
if block_outbound:
self.block_outbound()
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 block_outbound(self):
""" Blocks the connection from the container to outer network.
The iptables settings accept only 100.115.92.2 port 5555 (adb) and
all local connections, e.g. uiautomator.
"""
logging.info('Blocking outbound connection')
# Disable ipv6 temporarily since tests become flaky if ipv6
# outbound traffic is blocked with iptables.
_android_shell('sysctl -w net.ipv6.conf.all.disable_ipv6=1')
_android_shell('sysctl -w net.ipv6.conf.default.disable_ipv6=1')
# ipv4
_android_shell('iptables -I OUTPUT -j REJECT')
_android_shell('iptables -I OUTPUT -p tcp -s 100.115.92.2 '
'--sport 5555 '
'-j ACCEPT')
_android_shell('iptables -I OUTPUT -d localhost -j ACCEPT')
def unblock_outbound(self):
""" Unblocks the connection from the container to outer network.
The iptables settings are not permanent which means they reset on
each instance invocation. But we can still use this function to
unblock the outbound connections during the test if needed.
"""
logging.info('Unblocking outbound connection')
# ipv4
_android_shell('iptables -D OUTPUT -d localhost -j ACCEPT')
_android_shell('iptables -D OUTPUT -p tcp -s 100.115.92.2 '
'--sport 5555 '
'-j ACCEPT')
_android_shell('iptables -D OUTPUT -j REJECT')
# Re-enable ipv6.
_android_shell('sysctl -w net.ipv6.conf.all.disable_ipv6=0')
_android_shell('sysctl -w net.ipv6.conf.default.disable_ipv6=0')
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)