937 lines
34 KiB
Python
937 lines
34 KiB
Python
# 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)
|