diff options
author | csharp <csharp@chromium.org> | 2015-08-13 09:50:38 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-08-13 16:51:28 +0000 |
commit | b0e8b5904780100ed6793e4cca9215e3c453b857 (patch) | |
tree | beb3f40b0f2503721cfa565dca6d6e565d6ca41d /build | |
parent | 7950c3f9e4c82629208702eaca4719742880245e (diff) | |
download | chromium_src-b0e8b5904780100ed6793e4cca9215e3c453b857.zip chromium_src-b0e8b5904780100ed6793e4cca9215e3c453b857.tar.gz chromium_src-b0e8b5904780100ed6793e4cca9215e3c453b857.tar.bz2 |
Revert of [Android] Remove android_commands and android_testrunner. (patchset #3 id:40001 of https://codereview.chromium.org/1290773009/ )
Reason for revert:
This seems to have broken the Google Chrome Linux x64 build
Output:
FAILED: cd ../../content; python ../tools/isolate_driver.py check --isolated "../out/Release/telemetry_gpu_unittests.isolated" --isolate "telemetry_gpu_unittests.isolate" --path-variable DEPTH .. --path-variable PRODUCT_DIR "../out/Release " --extra-variable "version_full=46.0.2482.0" --config-variable "CONFIGURATION_NAME=Release" --config-variable "OS=linux" --config-variable "asan=0" --config-variable "branding=Chrome" --config-variable "chromeos=0" --config-variable "component=static_library" --config-variable "disable_nacl=0" --config-variable "enable_pepper_cdms=1" --config-variable "enable_plugins=1" --config-variable "fastbuild=0" --config-variable "icu_use_data_file_flag=1" --config-variable "internal_gles2_conform_tests=0" --config-variable "kasko=0" --config-variable "libpeer_target_type=static_library" --config-variable "lsan=0" --config-variable "msan=0" --config-variable "target_arch=x64" --config-variable "tsan=0" --config-variable "use_custom_libcxx=0" --config-variable "use_instrumented_libraries=0" --config-variable "use_prebuilt_instrumented_libraries=0" --config-variable "use_openssl=1" --config-variable "use_ozone=0" --config-variable "use_x11=1" --config-variable "v8_use_external_startup_data=1"
Failed to find an input file: /b/build/slave/google-chrome-rel-linux_64/build/src/third_party/android_testrunner/ is not a directory but ends with "/"
Original issue's description:
> [Android] Remove android_commands and android_testrunner.
>
> Good riddance.
>
> BUG=267773
>
> Committed: https://crrev.com/d71baced2e55f9f102189c490908e3c631a790e2
> Cr-Commit-Position: refs/heads/master@{#343210}
TBR=craigdh@chromium.org,nednguyen@google.com,perezju@chromium.org,sullivan@chromium.org,thakis@chromium.org,jbudorick@chromium.org
NOPRESUBMIT=true
NOTREECHECKS=true
NOTRY=true
BUG=267773
Review URL: https://codereview.chromium.org/1288993002
Cr-Commit-Position: refs/heads/master@{#343219}
Diffstat (limited to 'build')
-rw-r--r-- | build/android/PRESUBMIT.py | 4 | ||||
-rw-r--r-- | build/android/gyp/util/build_device.py | 7 | ||||
-rwxr-xr-x | build/android/provision_devices.py | 8 | ||||
-rw-r--r-- | build/android/pylib/android_commands.py | 1976 | ||||
-rw-r--r-- | build/android/pylib/android_commands_unittest.py | 191 | ||||
-rwxr-xr-x | build/android/pylib/device/battery_utils_test.py | 5 | ||||
-rw-r--r-- | build/android/pylib/device/decorators.py | 12 | ||||
-rw-r--r-- | build/android/pylib/device/decorators_test.py | 77 | ||||
-rw-r--r-- | build/android/pylib/device/device_utils.py | 7 | ||||
-rwxr-xr-x | build/android/pylib/device/device_utils_test.py | 12 | ||||
-rw-r--r-- | build/android/pylib/gtest/test_package_apk.py | 17 | ||||
-rw-r--r-- | build/android/pylib/instrumentation/test_runner.py | 2 | ||||
-rw-r--r-- | build/android/pylib/system_properties.py | 40 | ||||
-rw-r--r-- | build/android/pylib/valgrind_tools.py | 4 |
14 files changed, 2342 insertions, 20 deletions
diff --git a/build/android/PRESUBMIT.py b/build/android/PRESUBMIT.py index c5a7f80..c24a0c2 100644 --- a/build/android/PRESUBMIT.py +++ b/build/android/PRESUBMIT.py @@ -20,7 +20,9 @@ def CommonChecks(input_api, output_api): input_api, output_api, black_list=[r'pylib/symbols/.*\.py$', r'gyp/.*\.py$', r'gn/.*\.py'], - extra_paths_list=[J(), J('buildbot')])) + extra_paths_list=[ + J(), J('..', '..', 'third_party', 'android_testrunner'), + J('buildbot')])) output.extend(input_api.canned_checks.RunPylint( input_api, output_api, diff --git a/build/android/gyp/util/build_device.py b/build/android/gyp/util/build_device.py index e187c73..8ab1112 100644 --- a/build/android/gyp/util/build_device.py +++ b/build/android/gyp/util/build_device.py @@ -16,14 +16,11 @@ from util import build_utils BUILD_ANDROID_DIR = os.path.join(os.path.dirname(__file__), '..', '..') sys.path.append(BUILD_ANDROID_DIR) -from pylib.device import adb_wrapper +from pylib import android_commands from pylib.device import device_errors from pylib.device import device_utils - -def GetAttachedDevices(): - return [a.GetDeviceSerial() - for a in adb_wrapper.Devices()] +GetAttachedDevices = android_commands.GetAttachedDevices class BuildDevice(object): diff --git a/build/android/provision_devices.py b/build/android/provision_devices.py index cde01ad..427d1fa 100755 --- a/build/android/provision_devices.py +++ b/build/android/provision_devices.py @@ -29,6 +29,11 @@ from pylib.device import device_utils from pylib.utils import run_tests_helper from pylib.utils import timeout_retry +sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT, + 'third_party', 'android_testrunner')) +import errors + + _SYSTEM_WEBVIEW_PATHS = ['/system/app/webview', '/system/app/WebViewGoogle'] @@ -101,7 +106,8 @@ def ProvisionDevice(device, options): if should_run_phase(_PHASES.FINISH): run_phase(FinishProvisioning, reboot=False) - except device_errors.CommandTimeoutError: + except (errors.WaitForResponseTimedOutError, + device_errors.CommandTimeoutError): logging.exception('Timed out waiting for device %s. Adding to blacklist.', str(device)) device_blacklist.ExtendBlacklist([str(device)]) diff --git a/build/android/pylib/android_commands.py b/build/android/pylib/android_commands.py new file mode 100644 index 0000000..f7191f79 --- /dev/null +++ b/build/android/pylib/android_commands.py @@ -0,0 +1,1976 @@ +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Provides an interface to communicate with the device via the adb command. + +Assumes adb binary is currently on system path. + +Note that this module is deprecated. +""" +# TODO(jbudorick): Delete this file once no clients use it. + +# pylint: skip-file + +import collections +import datetime +import inspect +import logging +import os +import random +import re +import shlex +import signal +import subprocess +import sys +import tempfile +import time + +import cmd_helper +import constants +import system_properties +from utils import host_utils + +try: + from pylib import pexpect +except ImportError: + pexpect = None + +sys.path.append(os.path.join( + constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner')) +import adb_interface +import am_instrument_parser +import errors + +from pylib.device import device_blacklist +from pylib.device import device_errors + +# Pattern to search for the next whole line of pexpect output and capture it +# into a match group. We can't use ^ and $ for line start end with pexpect, +# see http://www.noah.org/python/pexpect/#doc for explanation why. +PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') + +# Set the adb shell prompt to be a unique marker that will [hopefully] not +# appear at the start of any line of a command's output. +SHELL_PROMPT = '~+~PQ\x17RS~+~' + +# Java properties file +LOCAL_PROPERTIES_PATH = constants.DEVICE_LOCAL_PROPERTIES_PATH + +# Property in /data/local.prop that controls Java assertions. +JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions' + +# Keycode "enum" suitable for passing to AndroidCommands.SendKey(). +KEYCODE_HOME = 3 +KEYCODE_BACK = 4 +KEYCODE_DPAD_UP = 19 +KEYCODE_DPAD_DOWN = 20 +KEYCODE_DPAD_RIGHT = 22 +KEYCODE_ENTER = 66 +KEYCODE_MENU = 82 + +MD5SUM_DEVICE_FOLDER = constants.TEST_EXECUTABLE_DIR + '/md5sum/' +MD5SUM_DEVICE_PATH = MD5SUM_DEVICE_FOLDER + 'md5sum_bin' + +PIE_WRAPPER_PATH = constants.TEST_EXECUTABLE_DIR + '/run_pie' + +CONTROL_USB_CHARGING_COMMANDS = [ + { + # Nexus 4 + 'witness_file': '/sys/module/pm8921_charger/parameters/disabled', + 'enable_command': 'echo 0 > /sys/module/pm8921_charger/parameters/disabled', + 'disable_command': + 'echo 1 > /sys/module/pm8921_charger/parameters/disabled', + }, + { + # Nexus 5 + # Setting the HIZ bit of the bq24192 causes the charger to actually ignore + # energy coming from USB. Setting the power_supply offline just updates the + # Android system to reflect that. + 'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT', + 'enable_command': ( + 'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && ' + 'echo 1 > /sys/class/power_supply/usb/online'), + 'disable_command': ( + 'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && ' + 'chmod 644 /sys/class/power_supply/usb/online && ' + 'echo 0 > /sys/class/power_supply/usb/online'), + }, +] + +class DeviceTempFile(object): + def __init__(self, android_commands, prefix='temp_file', suffix=''): + """Find an unused temporary file path in the devices external directory. + + When this object is closed, the file will be deleted on the device. + """ + self.android_commands = android_commands + while True: + # TODO(cjhopman): This could actually return the same file in multiple + # calls if the caller doesn't write to the files immediately. This is + # expected to never happen. + i = random.randint(0, 1000000) + self.name = '%s/%s-%d-%010d%s' % ( + android_commands.GetExternalStorage(), + prefix, int(time.time()), i, suffix) + if not android_commands.FileExistsOnDevice(self.name): + break + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def close(self): + self.android_commands.RunShellCommand('rm ' + self.name) + + +def GetAVDs(): + """Returns a list of AVDs.""" + re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE) + avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd'])) + return avds + +def ResetBadDevices(): + """Removes the blacklist that keeps track of bad devices for a current + build. + """ + device_blacklist.ResetBlacklist() + +def ExtendBadDevices(devices): + """Adds devices to the blacklist that keeps track of bad devices for a + current build. + + The devices listed in the bad devices file will not be returned by + GetAttachedDevices. + + Args: + devices: list of bad devices to be added to the bad devices file. + """ + device_blacklist.ExtendBlacklist(devices) + + +def GetAttachedDevices(hardware=True, emulator=True, offline=False): + """Returns a list of attached, android devices and emulators. + + If a preferred device has been set with ANDROID_SERIAL, it will be first in + the returned list. The arguments specify what devices to include in the list. + + Example output: + + * daemon not running. starting it now on port 5037 * + * daemon started successfully * + List of devices attached + 027c10494100b4d7 device + emulator-5554 offline + + Args: + hardware: Include attached actual devices that are online. + emulator: Include emulators (i.e. AVD's) currently on host. + offline: Include devices and emulators that are offline. + + Returns: List of devices. + """ + adb_devices_output = cmd_helper.GetCmdOutput([constants.GetAdbPath(), + 'devices']) + + re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) + online_devices = re_device.findall(adb_devices_output) + + re_device = re.compile('^(emulator-[0-9]+)\tdevice', re.MULTILINE) + emulator_devices = re_device.findall(adb_devices_output) + + re_device = re.compile('^([a-zA-Z0-9_:.-]+)\t(?:offline|unauthorized)$', + re.MULTILINE) + offline_devices = re_device.findall(adb_devices_output) + + devices = [] + # First determine list of online devices (e.g. hardware and/or emulator). + if hardware and emulator: + devices = online_devices + elif hardware: + devices = [device for device in online_devices + if device not in emulator_devices] + elif emulator: + devices = emulator_devices + + # Now add offline devices if offline is true + if offline: + devices = devices + offline_devices + + # Remove any devices in the blacklist. + blacklist = device_blacklist.ReadBlacklist() + if len(blacklist): + logging.info('Avoiding bad devices %s', ' '.join(blacklist)) + devices = [device for device in devices if device not in blacklist] + + preferred_device = os.environ.get('ANDROID_SERIAL') + if preferred_device in devices: + devices.remove(preferred_device) + devices.insert(0, preferred_device) + return devices + + +def IsDeviceAttached(device): + """Return true if the device is attached and online.""" + return device in GetAttachedDevices() + + +def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): + """Gets a list of files from `ls` command output. + + Python's os.walk isn't used because it doesn't work over adb shell. + + Args: + path: The path to list. + ls_output: A list of lines returned by an `ls -lR` command. + re_file: A compiled regular expression which parses a line into named groups + consisting of at minimum "filename", "date", "time", "size" and + optionally "timezone". + utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a + 2-digit string giving the number of UTC offset hours, and MM is a + 2-digit string giving the number of UTC offset minutes. If the input + utc_offset is None, will try to look for the value of "timezone" if it + is specified in re_file. + + Returns: + A dict of {"name": (size, lastmod), ...} where: + name: The file name relative to |path|'s directory. + size: The file size in bytes (0 for directories). + lastmod: The file last modification date in UTC. + """ + re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path)) + path_dir = os.path.dirname(path) + + current_dir = '' + files = {} + for line in ls_output: + directory_match = re_directory.match(line) + if directory_match: + current_dir = directory_match.group('dir') + continue + file_match = re_file.match(line) + if file_match: + filename = os.path.join(current_dir, file_match.group('filename')) + if filename.startswith(path_dir): + filename = filename[len(path_dir) + 1:] + lastmod = datetime.datetime.strptime( + file_match.group('date') + ' ' + file_match.group('time')[:5], + '%Y-%m-%d %H:%M') + if not utc_offset and 'timezone' in re_file.groupindex: + utc_offset = file_match.group('timezone') + if isinstance(utc_offset, str) and len(utc_offset) == 5: + utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), + minutes=int(utc_offset[3:5])) + if utc_offset[0:1] == '-': + utc_delta = -utc_delta + lastmod -= utc_delta + files[filename] = (int(file_match.group('size')), lastmod) + return files + + +def _ParseMd5SumOutput(md5sum_output): + """Returns a list of tuples from the provided md5sum output. + + Args: + md5sum_output: output directly from md5sum binary. + + Returns: + List of namedtuples with attributes |hash| and |path|, where |path| is the + absolute path to the file with an Md5Sum of |hash|. + """ + HashAndPath = collections.namedtuple('HashAndPath', ['hash', 'path']) + split_lines = [line.split(' ') for line in md5sum_output] + return [HashAndPath._make(s) for s in split_lines if len(s) == 2] + + +def _HasAdbPushSucceeded(command_output): + """Returns whether adb push has succeeded from the provided output.""" + # TODO(frankf): We should look at the return code instead of the command + # output for many of the commands in this file. + if not command_output: + return True + # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" + # Errors look like this: "failed to copy ... " + if not re.search('^[0-9]', command_output.splitlines()[-1]): + logging.critical('PUSH FAILED: ' + command_output) + return False + return True + + +def GetLogTimestamp(log_line, year): + """Returns the timestamp of the given |log_line| in the given year.""" + try: + return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), + '%Y-%m-%d %H:%M:%S.%f') + except (ValueError, IndexError): + logging.critical('Error reading timestamp from ' + log_line) + return None + + +class AndroidCommands(object): + """Helper class for communicating with Android device via adb.""" + + def __init__(self, device=None): + """Constructor. + + Args: + device: If given, adb commands are only send to the device of this ID. + Otherwise commands are sent to all attached devices. + """ + self._adb = adb_interface.AdbInterface(constants.GetAdbPath()) + if device: + self._adb.SetTargetSerial(device) + self._device = device + self._logcat = None + self.logcat_process = None + self._logcat_tmpoutfile = None + self._pushed_files = [] + self._device_utc_offset = None + self._potential_push_size = 0 + self._actual_push_size = 0 + self._external_storage = '' + self._util_wrapper = '' + self._system_properties = system_properties.SystemProperties(self.Adb()) + self._push_if_needed_cache = {} + self._control_usb_charging_command = { + 'command': None, + 'cached': False, + } + self._protected_file_access_method_initialized = None + self._privileged_command_runner = None + self._pie_wrapper = None + + @property + def system_properties(self): + return self._system_properties + + def _LogShell(self, cmd): + """Logs the adb shell command.""" + if self._device: + device_repr = self._device[-4:] + else: + device_repr = '????' + logging.info('[%s]> %s', device_repr, cmd) + + def Adb(self): + """Returns our AdbInterface to avoid us wrapping all its methods.""" + # TODO(tonyg): Goal should be to git rid of this method by making this API + # complete and alleviating the need. + return self._adb + + def GetDevice(self): + """Returns the device serial.""" + return self._device + + def IsOnline(self): + """Checks whether the device is online. + + Returns: + True if device is in 'device' mode, False otherwise. + """ + # TODO(aurimas): revert to using adb get-state when android L adb is fixed. + #out = self._adb.SendCommand('get-state') + #return out.strip() == 'device' + + out = self._adb.SendCommand('devices') + for line in out.split('\n'): + if self._device in line and 'device' in line: + return True + return False + + def IsRootEnabled(self): + """Checks if root is enabled on the device.""" + root_test_output = self.RunShellCommand('ls /root') or [''] + return not 'Permission denied' in root_test_output[0] + + def EnableAdbRoot(self): + """Enables adb root on the device. + + Returns: + True: if output from executing adb root was as expected. + False: otherwise. + """ + if self.GetBuildType() == 'user': + logging.warning("Can't enable root in production builds with type user") + return False + else: + return_value = self._adb.EnableAdbRoot() + # EnableAdbRoot inserts a call for wait-for-device only when adb logcat + # output matches what is expected. Just to be safe add a call to + # wait-for-device. + self._adb.SendCommand('wait-for-device') + return return_value + + def GetDeviceYear(self): + """Returns the year information of the date on device.""" + return self.RunShellCommand('date +%Y')[0] + + def GetExternalStorage(self): + if not self._external_storage: + self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0] + if not self._external_storage: + raise device_errors.CommandFailedError( + ['shell', "'echo $EXTERNAL_STORAGE'"], + 'Unable to find $EXTERNAL_STORAGE') + return self._external_storage + + def WaitForDevicePm(self, timeout=120): + """Blocks until the device's package manager is available. + + To workaround http://b/5201039, we restart the shell and retry if the + package manager isn't back after 120 seconds. + + Raises: + errors.WaitForResponseTimedOutError after max retries reached. + """ + last_err = None + retries = 3 + while retries: + try: + self._adb.WaitForDevicePm(wait_time=timeout) + return # Success + except errors.WaitForResponseTimedOutError as e: + last_err = e + logging.warning('Restarting and retrying after timeout: %s', e) + retries -= 1 + self.RestartShell() + raise last_err # Only reached after max retries, re-raise the last error. + + def RestartShell(self): + """Restarts the shell on the device. Does not block for it to return.""" + self.RunShellCommand('stop') + self.RunShellCommand('start') + + def Reboot(self, full_reboot=True): + """Reboots the device and waits for the package manager to return. + + Args: + full_reboot: Whether to fully reboot the device or just restart the shell. + """ + # TODO(torne): hive can't reboot the device either way without breaking the + # connection; work out if we can handle this better + if os.environ.get('USING_HIVE'): + logging.warning('Ignoring reboot request as we are on hive') + return + if full_reboot or not self.IsRootEnabled(): + self._adb.SendCommand('reboot') + self._system_properties = system_properties.SystemProperties(self.Adb()) + timeout = 300 + retries = 1 + # Wait for the device to disappear. + while retries < 10 and self.IsOnline(): + time.sleep(1) + retries += 1 + else: + self.RestartShell() + timeout = 120 + # To run tests we need at least the package manager and the sd card (or + # other external storage) to be ready. + self.WaitForDevicePm(timeout) + self.WaitForSdCardReady(timeout) + + def Shutdown(self): + """Shuts down the device.""" + self._adb.SendCommand('reboot -p') + self._system_properties = system_properties.SystemProperties(self.Adb()) + + def Uninstall(self, package): + """Uninstalls the specified package from the device. + + Args: + package: Name of the package to remove. + + Returns: + A status string returned by adb uninstall + """ + uninstall_command = 'uninstall %s' % package + + self._LogShell(uninstall_command) + return self._adb.SendCommand(uninstall_command, timeout_time=60) + + def Install(self, package_file_path, reinstall=False): + """Installs the specified package to the device. + + Args: + package_file_path: Path to .apk file to install. + reinstall: Reinstall an existing apk, keeping the data. + + Returns: + A status string returned by adb install + """ + assert os.path.isfile(package_file_path), ('<%s> is not file' % + package_file_path) + + install_cmd = ['install'] + + if reinstall: + install_cmd.append('-r') + + install_cmd.append(package_file_path) + install_cmd = ' '.join(install_cmd) + + self._LogShell(install_cmd) + return self._adb.SendCommand(install_cmd, + timeout_time=2 * 60, + retry_count=0) + + def ManagedInstall(self, apk_path, keep_data=False, package_name=None, + reboots_on_timeout=2): + """Installs specified package and reboots device on timeouts. + + If package_name is supplied, checks if the package is already installed and + doesn't reinstall if the apk md5sums match. + + Args: + apk_path: Path to .apk file to install. + keep_data: Reinstalls instead of uninstalling first, preserving the + application data. + package_name: Package name (only needed if keep_data=False). + reboots_on_timeout: number of time to reboot if package manager is frozen. + """ + # Check if package is already installed and up to date. + if package_name: + installed_apk_path = self.GetApplicationPath(package_name) + if (installed_apk_path and + not self.GetFilesChanged(apk_path, installed_apk_path, + ignore_filenames=True)): + logging.info('Skipped install: identical %s APK already installed' % + package_name) + return + # Install. + reboots_left = reboots_on_timeout + while True: + try: + if not keep_data: + assert package_name + self.Uninstall(package_name) + install_status = self.Install(apk_path, reinstall=keep_data) + if 'Success' in install_status: + return + else: + raise Exception('Install failure: %s' % install_status) + except errors.WaitForResponseTimedOutError: + print '@@@STEP_WARNINGS@@@' + logging.info('Timeout on installing %s on device %s', apk_path, + self._device) + + if reboots_left <= 0: + raise Exception('Install timed out') + + # Force a hard reboot on last attempt + self.Reboot(full_reboot=(reboots_left == 1)) + reboots_left -= 1 + + def MakeSystemFolderWritable(self): + """Remounts the /system folder rw.""" + out = self._adb.SendCommand('remount') + if out.strip() != 'remount succeeded': + raise errors.MsgException('Remount failed: %s' % out) + + def RestartAdbdOnDevice(self): + logging.info('Restarting adbd on the device...') + with DeviceTempFile(self, suffix=".sh") as temp_script_file: + host_script_path = os.path.join(constants.DIR_SOURCE_ROOT, + 'build', + 'android', + 'pylib', + 'restart_adbd.sh') + self._adb.Push(host_script_path, temp_script_file.name) + self.RunShellCommand('. %s' % temp_script_file.name) + self._adb.SendCommand('wait-for-device') + + def RestartAdbServer(self): + """Restart the adb server.""" + ret = self.KillAdbServer() + if ret != 0: + raise errors.MsgException('KillAdbServer: %d' % ret) + + ret = self.StartAdbServer() + if ret != 0: + raise errors.MsgException('StartAdbServer: %d' % ret) + + @staticmethod + def KillAdbServer(): + """Kill adb server.""" + adb_cmd = [constants.GetAdbPath(), 'kill-server'] + ret = cmd_helper.RunCmd(adb_cmd) + retry = 0 + while retry < 3: + ret, _ = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb']) + if ret != 0: + # pgrep didn't find adb, kill-server succeeded. + return 0 + retry += 1 + time.sleep(retry) + return ret + + def StartAdbServer(self): + """Start adb server.""" + adb_cmd = ['taskset', '-c', '0', constants.GetAdbPath(), 'start-server'] + ret, _ = cmd_helper.GetCmdStatusAndOutput(adb_cmd) + retry = 0 + while retry < 3: + ret, _ = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb']) + if ret == 0: + # pgrep found adb, start-server succeeded. + # Waiting for device to reconnect before returning success. + self._adb.SendCommand('wait-for-device') + return 0 + retry += 1 + time.sleep(retry) + return ret + + def WaitForSystemBootCompleted(self, wait_time): + """Waits for targeted system's boot_completed flag to be set. + + Args: + wait_time: time in seconds to wait + + Raises: + WaitForResponseTimedOutError if wait_time elapses and flag still not + set. + """ + logging.info('Waiting for system boot completed...') + self._adb.SendCommand('wait-for-device') + # Now the device is there, but system not boot completed. + # Query the sys.boot_completed flag with a basic command + boot_completed = False + attempts = 0 + wait_period = 5 + while not boot_completed and (attempts * wait_period) < wait_time: + output = self.system_properties['sys.boot_completed'] + output = output.strip() + if output == '1': + boot_completed = True + else: + # If 'error: xxx' returned when querying the flag, it means + # adb server lost the connection to the emulator, so restart the adb + # server. + if 'error:' in output: + self.RestartAdbServer() + time.sleep(wait_period) + attempts += 1 + if not boot_completed: + raise errors.WaitForResponseTimedOutError( + 'sys.boot_completed flag was not set after %s seconds' % wait_time) + + def WaitForSdCardReady(self, timeout_time): + """Wait for the SD card ready before pushing data into it.""" + logging.info('Waiting for SD card ready...') + sdcard_ready = False + attempts = 0 + wait_period = 5 + external_storage = self.GetExternalStorage() + while not sdcard_ready and attempts * wait_period < timeout_time: + output = self.RunShellCommand('ls ' + external_storage) + if output: + sdcard_ready = True + else: + time.sleep(wait_period) + attempts += 1 + if not sdcard_ready: + raise errors.WaitForResponseTimedOutError( + 'SD card not ready after %s seconds' % timeout_time) + + def GetAndroidToolStatusAndOutput(self, command, lib_path=None, *args, **kw): + """Runs a native Android binary, wrapping the command as necessary. + + This is a specialization of GetShellCommandStatusAndOutput, which is meant + for running tools/android/ binaries and handle properly: (1) setting the + lib path (for component=shared_library), (2) using the PIE wrapper on ICS. + See crbug.com/373219 for more context. + + Args: + command: String containing the command to send. + lib_path: (optional) path to the folder containing the dependent libs. + Same other arguments of GetCmdStatusAndOutput. + """ + # The first time this command is run the device is inspected to check + # whether a wrapper for running PIE executable is needed (only Android ICS) + # or not. The results is cached, so the wrapper is pushed only once. + if self._pie_wrapper is None: + # None: did not check; '': did check and not needed; '/path': use /path. + self._pie_wrapper = '' + if self.GetBuildId().startswith('I'): # Ixxxx = Android ICS. + run_pie_dist_path = os.path.join(constants.GetOutDirectory(), 'run_pie') + assert os.path.exists(run_pie_dist_path), 'Please build run_pie' + # The PIE loader must be pushed manually (i.e. no PushIfNeeded) because + # PushIfNeeded requires md5sum and md5sum requires the wrapper as well. + adb_command = 'push %s %s' % (run_pie_dist_path, PIE_WRAPPER_PATH) + assert _HasAdbPushSucceeded(self._adb.SendCommand(adb_command)) + self._pie_wrapper = PIE_WRAPPER_PATH + + if self._pie_wrapper: + command = '%s %s' % (self._pie_wrapper, command) + if lib_path: + command = 'LD_LIBRARY_PATH=%s %s' % (lib_path, command) + return self.GetShellCommandStatusAndOutput(command, *args, **kw) + + # It is tempting to turn this function into a generator, however this is not + # possible without using a private (local) adb_shell instance (to ensure no + # other command interleaves usage of it), which would defeat the main aim of + # being able to reuse the adb shell instance across commands. + def RunShellCommand(self, command, timeout_time=20, log_result=False): + """Send a command to the adb shell and return the result. + + Args: + command: String containing the shell command to send. + timeout_time: Number of seconds to wait for command to respond before + retrying, used by AdbInterface.SendShellCommand. + log_result: Boolean to indicate whether we should log the result of the + shell command. + + Returns: + list containing the lines of output received from running the command + """ + self._LogShell(command) + if "'" in command: + command = command.replace('\'', '\'\\\'\'') + result = self._adb.SendShellCommand( + "'%s'" % command, timeout_time).splitlines() + # TODO(b.kelemen): we should really be able to drop the stderr of the + # command or raise an exception based on what the caller wants. + result = [ l for l in result if not l.startswith('WARNING') ] + if ['error: device not found'] == result: + raise errors.DeviceUnresponsiveError('device not found') + if log_result: + self._LogShell('\n'.join(result)) + return result + + def GetShellCommandStatusAndOutput(self, command, timeout_time=20, + log_result=False): + """See RunShellCommand() above. + + Returns: + The tuple (exit code, list of output lines). + """ + lines = self.RunShellCommand( + command + '; echo %$?', timeout_time, log_result) + last_line = lines[-1] + status_pos = last_line.rfind('%') + assert status_pos >= 0 + status = int(last_line[status_pos + 1:]) + if status_pos == 0: + lines = lines[:-1] + else: + lines = lines[:-1] + [last_line[:status_pos]] + return (status, lines) + + def KillAll(self, process, signum=9, with_su=False): + """Android version of killall, connected via adb. + + Args: + process: name of the process to kill off. + signum: signal to use, 9 (SIGKILL) by default. + with_su: wether or not to use su to kill the processes. + + Returns: + the number of processes killed + """ + pids = self.ExtractPid(process) + if pids: + cmd = 'kill -%d %s' % (signum, ' '.join(pids)) + if with_su: + self.RunShellCommandWithSU(cmd) + else: + self.RunShellCommand(cmd) + return len(pids) + + def KillAllBlocking(self, process, timeout_sec, signum=9, with_su=False): + """Blocking version of killall, connected via adb. + + This waits until no process matching the corresponding name appears in ps' + output anymore. + + Args: + process: name of the process to kill off + timeout_sec: the timeout in seconds + signum: same as |KillAll| + with_su: same as |KillAll| + Returns: + the number of processes killed + """ + processes_killed = self.KillAll(process, signum=signum, with_su=with_su) + if processes_killed: + elapsed = 0 + wait_period = 0.1 + # Note that this doesn't take into account the time spent in ExtractPid(). + while self.ExtractPid(process) and elapsed < timeout_sec: + time.sleep(wait_period) + elapsed += wait_period + if elapsed >= timeout_sec: + return processes_killed - self.ExtractPid(process) + return processes_killed + + @staticmethod + def _GetActivityCommand(package, activity, wait_for_completion, action, + category, data, extras, trace_file_name, force_stop, + flags): + """Creates command to start |package|'s activity on the device. + + Args - as for StartActivity + + Returns: + the command to run on the target to start the activity + """ + cmd = 'am start -a %s' % action + if force_stop: + cmd += ' -S' + if wait_for_completion: + cmd += ' -W' + if category: + cmd += ' -c %s' % category + if package and activity: + cmd += ' -n %s/%s' % (package, activity) + if data: + cmd += ' -d "%s"' % data + if extras: + for key in extras: + value = extras[key] + if isinstance(value, str): + cmd += ' --es' + elif isinstance(value, bool): + cmd += ' --ez' + elif isinstance(value, int): + cmd += ' --ei' + else: + raise NotImplementedError( + 'Need to teach StartActivity how to pass %s extras' % type(value)) + cmd += ' %s %s' % (key, value) + if trace_file_name: + cmd += ' --start-profiler ' + trace_file_name + if flags: + cmd += ' -f %s' % flags + return cmd + + def StartActivity(self, package, activity, wait_for_completion=False, + action='android.intent.action.VIEW', + category=None, data=None, + extras=None, trace_file_name=None, + force_stop=False, flags=None): + """Starts |package|'s activity on the device. + + Args: + package: Name of package to start (e.g. 'com.google.android.apps.chrome'). + activity: Name of activity (e.g. '.Main' or + 'com.google.android.apps.chrome.Main'). + wait_for_completion: wait for the activity to finish launching (-W flag). + action: string (e.g. "android.intent.action.MAIN"). Default is VIEW. + category: string (e.g. "android.intent.category.HOME") + data: Data string to pass to activity (e.g. 'http://www.example.com/'). + extras: Dict of extras to pass to activity. Values are significant. + trace_file_name: If used, turns on and saves the trace to this file name. + force_stop: force stop the target app before starting the activity (-S + flag). + Returns: + The output of the underlying command as a list of lines. + """ + cmd = self._GetActivityCommand(package, activity, wait_for_completion, + action, category, data, extras, + trace_file_name, force_stop, flags) + return self.RunShellCommand(cmd) + + def StartActivityTimed(self, package, activity, wait_for_completion=False, + action='android.intent.action.VIEW', + category=None, data=None, + extras=None, trace_file_name=None, + force_stop=False, flags=None): + """Starts |package|'s activity on the device, returning the start time + + Args - as for StartActivity + + Returns: + A tuple containing: + - the output of the underlying command as a list of lines, and + - a timestamp string for the time at which the activity started + """ + cmd = self._GetActivityCommand(package, activity, wait_for_completion, + action, category, data, extras, + trace_file_name, force_stop, flags) + self.StartMonitoringLogcat() + out = self.RunShellCommand('log starting activity; ' + cmd) + activity_started_re = re.compile('.*starting activity.*') + m = self.WaitForLogMatch(activity_started_re, None) + assert m + start_line = m.group(0) + return (out, GetLogTimestamp(start_line, self.GetDeviceYear())) + + def StartCrashUploadService(self, package): + # TODO(frankf): We really need a python wrapper around Intent + # to be shared with StartActivity/BroadcastIntent. + cmd = ( + 'am startservice -a %s.crash.ACTION_FIND_ALL -n ' + '%s/%s.crash.MinidumpUploadService' % + (constants.PACKAGE_INFO['chrome'].package, + package, + constants.PACKAGE_INFO['chrome'].package)) + am_output = self.RunShellCommandWithSU(cmd) + assert am_output and 'Starting' in am_output[-1], ( + 'Service failed to start: %s' % am_output) + time.sleep(15) + + def BroadcastIntent(self, package, intent, *args): + """Send a broadcast intent. + + Args: + package: Name of package containing the intent. + intent: Name of the intent. + args: Optional extra arguments for the intent. + """ + cmd = 'am broadcast -a %s.%s %s' % (package, intent, ' '.join(args)) + self.RunShellCommand(cmd) + + def GoHome(self): + """Tell the device to return to the home screen. Blocks until completion.""" + self.RunShellCommand('am start -W ' + '-a android.intent.action.MAIN -c android.intent.category.HOME') + + def CloseApplication(self, package): + """Attempt to close down the application, using increasing violence. + + Args: + package: Name of the process to kill off, e.g. + com.google.android.apps.chrome + """ + self.RunShellCommand('am force-stop ' + package) + + def GetApplicationPath(self, package): + """Get the installed apk path on the device for the given package. + + Args: + package: Name of the package. + + Returns: + Path to the apk on the device if it exists, None otherwise. + """ + pm_path_output = self.RunShellCommand('pm path ' + package) + # The path output contains anything if and only if the package + # exists. + if pm_path_output: + # pm_path_output is of the form: "package:/path/to/foo.apk" + return pm_path_output[0].split(':')[1] + else: + return None + + def ClearApplicationState(self, package): + """Closes and clears all state for the given |package|.""" + # Check that the package exists before clearing it. Necessary because + # calling pm clear on a package that doesn't exist may never return. + pm_path_output = self.RunShellCommand('pm path ' + package) + # The path output only contains anything if and only if the package exists. + if pm_path_output: + self.RunShellCommand('pm clear ' + package) + + def SendKeyEvent(self, keycode): + """Sends keycode to the device. + + Args: + keycode: Numeric keycode to send (see "enum" at top of file). + """ + self.RunShellCommand('input keyevent %d' % keycode) + + def _RunMd5Sum(self, host_path, device_path): + """Gets the md5sum of a host path and device path. + + Args: + host_path: Path (file or directory) on the host. + device_path: Path on the device. + + Returns: + A tuple containing lists of the host and device md5sum results as + created by _ParseMd5SumOutput(). + """ + md5sum_dist_path = os.path.join(constants.GetOutDirectory(), + 'md5sum_dist') + assert os.path.exists(md5sum_dist_path), 'Please build md5sum.' + md5sum_dist_mtime = os.stat(md5sum_dist_path).st_mtime + if (md5sum_dist_path not in self._push_if_needed_cache or + self._push_if_needed_cache[md5sum_dist_path] != md5sum_dist_mtime): + command = 'push %s %s' % (md5sum_dist_path, MD5SUM_DEVICE_FOLDER) + assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) + self._push_if_needed_cache[md5sum_dist_path] = md5sum_dist_mtime + + (_, md5_device_output) = self.GetAndroidToolStatusAndOutput( + self._util_wrapper + ' ' + MD5SUM_DEVICE_PATH + ' ' + device_path, + lib_path=MD5SUM_DEVICE_FOLDER, + timeout_time=2 * 60) + device_hash_tuples = _ParseMd5SumOutput(md5_device_output) + assert os.path.exists(host_path), 'Local path not found %s' % host_path + md5sum_output = cmd_helper.GetCmdOutput( + [os.path.join(constants.GetOutDirectory(), 'md5sum_bin_host'), + host_path]) + host_hash_tuples = _ParseMd5SumOutput(md5sum_output.splitlines()) + return (host_hash_tuples, device_hash_tuples) + + def GetFilesChanged(self, host_path, device_path, ignore_filenames=False): + """Compares the md5sum of a host path against a device path. + + Note: Ignores extra files on the device. + + Args: + host_path: Path (file or directory) on the host. + device_path: Path on the device. + ignore_filenames: If True only the file contents are considered when + checking whether a file has changed, otherwise the relative path + must also match. + + Returns: + A list of tuples of the form (host_path, device_path) for files whose + md5sums do not match. + """ + + # Md5Sum resolves symbolic links in path names so the calculation of + # relative path names from its output will need the real path names of the + # base directories. Having calculated these they are used throughout the + # function since this makes us less subject to any future changes to Md5Sum. + real_host_path = os.path.realpath(host_path) + real_device_path = self.RunShellCommand('realpath "%s"' % device_path)[0] + + host_hash_tuples, device_hash_tuples = self._RunMd5Sum( + real_host_path, real_device_path) + + if len(host_hash_tuples) > len(device_hash_tuples): + logging.info('%s files do not exist on the device' % + (len(host_hash_tuples) - len(device_hash_tuples))) + + host_rel = [(os.path.relpath(os.path.normpath(t.path), real_host_path), + t.hash) + for t in host_hash_tuples] + + if os.path.isdir(real_host_path): + def RelToRealPaths(rel_path): + return (os.path.join(real_host_path, rel_path), + os.path.join(real_device_path, rel_path)) + else: + assert len(host_rel) == 1 + def RelToRealPaths(_): + return (real_host_path, real_device_path) + + if ignore_filenames: + # If we are ignoring file names, then we want to push any file for which + # a file with an equivalent MD5 sum does not exist on the device. + device_hashes = set([h.hash for h in device_hash_tuples]) + ShouldPush = lambda p, h: h not in device_hashes + else: + # Otherwise, we want to push any file on the host for which a file with + # an equivalent MD5 sum does not exist at the same relative path on the + # device. + device_rel = dict([(os.path.relpath(os.path.normpath(t.path), + real_device_path), + t.hash) + for t in device_hash_tuples]) + ShouldPush = lambda p, h: p not in device_rel or h != device_rel[p] + + return [RelToRealPaths(path) for path, host_hash in host_rel + if ShouldPush(path, host_hash)] + + def PushIfNeeded(self, host_path, device_path): + """Pushes |host_path| to |device_path|. + + Works for files and directories. This method skips copying any paths in + |test_data_paths| that already exist on the device with the same hash. + + All pushed files can be removed by calling RemovePushedFiles(). + """ + MAX_INDIVIDUAL_PUSHES = 50 + if not os.path.exists(host_path): + raise device_errors.CommandFailedError( + 'Local path not found %s' % host_path, device=str(self)) + + # See if the file on the host changed since the last push (if any) and + # return early if it didn't. Note that this shortcut assumes that the tests + # on the device don't modify the files. + if not os.path.isdir(host_path): + if host_path in self._push_if_needed_cache: + host_path_mtime = self._push_if_needed_cache[host_path] + if host_path_mtime == os.stat(host_path).st_mtime: + return + + size = host_utils.GetRecursiveDiskUsage(host_path) + self._pushed_files.append(device_path) + self._potential_push_size += size + + if os.path.isdir(host_path): + self.RunShellCommand('mkdir -p "%s"' % device_path) + + changed_files = self.GetFilesChanged(host_path, device_path) + logging.info('Found %d files that need to be pushed to %s', + len(changed_files), device_path) + if not changed_files: + return + + def Push(host, device): + # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout + # of 60 seconds which isn't sufficient for a lot of users of this method. + push_command = 'push %s %s' % (host, device) + self._LogShell(push_command) + + # Retry push with increasing backoff if the device is busy. + retry = 0 + while True: + output = self._adb.SendCommand(push_command, timeout_time=30 * 60) + if _HasAdbPushSucceeded(output): + if not os.path.isdir(host_path): + self._push_if_needed_cache[host] = os.stat(host).st_mtime + return + if retry < 3: + retry += 1 + wait_time = 5 * retry + logging.error('Push failed, retrying in %d seconds: %s' % + (wait_time, output)) + time.sleep(wait_time) + else: + raise Exception('Push failed: %s' % output) + + diff_size = 0 + if len(changed_files) <= MAX_INDIVIDUAL_PUSHES: + diff_size = sum(host_utils.GetRecursiveDiskUsage(f[0]) + for f in changed_files) + + # TODO(craigdh): Replace this educated guess with a heuristic that + # approximates the push time for each method. + if len(changed_files) > MAX_INDIVIDUAL_PUSHES or diff_size > 0.5 * size: + self._actual_push_size += size + Push(host_path, device_path) + else: + for f in changed_files: + Push(f[0], f[1]) + self._actual_push_size += diff_size + + def GetPushSizeInfo(self): + """Get total size of pushes to the device done via PushIfNeeded() + + Returns: + A tuple: + 1. Total size of push requests to PushIfNeeded (MB) + 2. Total size that was actually pushed (MB) + """ + return (self._potential_push_size, self._actual_push_size) + + def GetFileContents(self, filename, log_result=False): + """Gets contents from the file specified by |filename|.""" + return self.RunShellCommand('cat "%s" 2>/dev/null' % filename, + log_result=log_result) + + def SetFileContents(self, filename, contents): + """Writes |contents| to the file specified by |filename|.""" + with tempfile.NamedTemporaryFile() as f: + f.write(contents) + f.flush() + self._adb.Push(f.name, filename) + + def RunShellCommandWithSU(self, command, timeout_time=20, log_result=False): + return self.RunShellCommand('su -c %s' % command, timeout_time, log_result) + + def CanAccessProtectedFileContents(self): + """Returns True if Get/SetProtectedFileContents would work via "su" or adb + shell running as root. + + Devices running user builds don't have adb root, but may provide "su" which + can be used for accessing protected files. + """ + return (self._GetProtectedFileCommandRunner() != None) + + def _GetProtectedFileCommandRunner(self): + """Finds the best method to access protected files on the device. + + Returns: + 1. None when privileged files cannot be accessed on the device. + 2. Otherwise: A function taking a single parameter: a string with command + line arguments. Running that function executes the command with + the appropriate method. + """ + if self._protected_file_access_method_initialized: + return self._privileged_command_runner + + self._privileged_command_runner = None + self._protected_file_access_method_initialized = True + + for cmd in [self.RunShellCommand, self.RunShellCommandWithSU]: + # Get contents of the auxv vector for the init(8) process from a small + # binary file that always exists on linux and is always read-protected. + contents = cmd('cat /proc/1/auxv') + # The leading 4 or 8-bytes of auxv vector is a_type. There are not many + # reserved a_type values, hence byte 2 must always be '\0' for a realistic + # auxv. See /usr/include/elf.h. + if len(contents) > 0 and (contents[0][2] == '\0'): + self._privileged_command_runner = cmd + break + return self._privileged_command_runner + + def GetProtectedFileContents(self, filename): + """Gets contents from the protected file specified by |filename|. + + This is potentially less efficient than GetFileContents. + """ + command = 'cat "%s" 2> /dev/null' % filename + command_runner = self._GetProtectedFileCommandRunner() + if command_runner: + return command_runner(command) + else: + logging.warning('Could not access protected file: %s' % filename) + return [] + + def SetProtectedFileContents(self, filename, contents): + """Writes |contents| to the protected file specified by |filename|. + + This is less efficient than SetFileContents. + """ + with DeviceTempFile(self) as temp_file: + with DeviceTempFile(self, suffix=".sh") as temp_script: + # Put the contents in a temporary file + self.SetFileContents(temp_file.name, contents) + # Create a script to copy the file contents to its final destination + self.SetFileContents(temp_script.name, + 'cat %s > %s' % (temp_file.name, filename)) + + command = 'sh %s' % temp_script.name + command_runner = self._GetProtectedFileCommandRunner() + if command_runner: + return command_runner(command) + else: + logging.warning( + 'Could not set contents of protected file: %s' % filename) + + + def RemovePushedFiles(self): + """Removes all files pushed with PushIfNeeded() from the device.""" + for p in self._pushed_files: + self.RunShellCommand('rm -r %s' % p, timeout_time=2 * 60) + + def ListPathContents(self, path): + """Lists files in all subdirectories of |path|. + + Args: + path: The path to list. + + Returns: + A dict of {"name": (size, lastmod), ...}. + """ + # Example output: + # /foo/bar: + # -rw-r----- user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt + re_file = re.compile('^-(?P<perms>[^\s]+)\s+' + '(?P<user>[^\s]+)\s+' + '(?P<group>[^\s]+)\s+' + '(?P<size>[^\s]+)\s+' + '(?P<date>[^\s]+)\s+' + '(?P<time>[^\s]+)\s+' + '(?P<filename>[^\s]+)$') + return _GetFilesFromRecursiveLsOutput( + path, self.RunShellCommand('ls -lR %s' % path), re_file, + self.GetUtcOffset()) + + def GetUtcOffset(self): + if not self._device_utc_offset: + self._device_utc_offset = self.RunShellCommand('date +%z')[0] + return self._device_utc_offset + + def SetJavaAssertsEnabled(self, enable): + """Sets or removes the device java assertions property. + + Args: + enable: If True the property will be set. + + Returns: + True if the file was modified (reboot is required for it to take effect). + """ + # First ensure the desired property is persisted. + temp_props_file = tempfile.NamedTemporaryFile() + properties = '' + if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name): + with open(temp_props_file.name) as f: + properties = f.read() + re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + + r'\s*=\s*all\s*$', re.MULTILINE) + if enable != bool(re.search(re_search, properties)): + re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + + r'\s*=\s*\w+\s*$', re.MULTILINE) + properties = re.sub(re_replace, '', properties) + if enable: + properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY + + file(temp_props_file.name, 'w').write(properties) + self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH) + + # Next, check the current runtime value is what we need, and + # if not, set it and report that a reboot is required. + was_set = 'all' in self.system_properties[JAVA_ASSERT_PROPERTY] + if was_set == enable: + return False + self.system_properties[JAVA_ASSERT_PROPERTY] = enable and 'all' or '' + return True + + def GetBuildId(self): + """Returns the build ID of the system (e.g. JRM79C).""" + build_id = self.system_properties['ro.build.id'] + assert build_id + return build_id + + def GetBuildType(self): + """Returns the build type of the system (e.g. eng).""" + build_type = self.system_properties['ro.build.type'] + assert build_type + return build_type + + def GetBuildProduct(self): + """Returns the build product of the device (e.g. maguro).""" + build_product = self.system_properties['ro.build.product'] + assert build_product + return build_product + + def GetProductName(self): + """Returns the product name of the device (e.g. takju).""" + name = self.system_properties['ro.product.name'] + assert name + return name + + def GetBuildFingerprint(self): + """Returns the build fingerprint of the device.""" + build_fingerprint = self.system_properties['ro.build.fingerprint'] + assert build_fingerprint + return build_fingerprint + + def GetDescription(self): + """Returns the description of the system. + + For example, "yakju-userdebug 4.1 JRN54F 364167 dev-keys". + """ + description = self.system_properties['ro.build.description'] + assert description + return description + + def GetProductModel(self): + """Returns the name of the product model (e.g. "Galaxy Nexus") """ + model = self.system_properties['ro.product.model'] + assert model + return model + + def GetWifiIP(self): + """Returns the wifi IP on the device.""" + wifi_ip = self.system_properties['dhcp.wlan0.ipaddress'] + # Do not assert here. Devices (e.g. emulators) may not have a WifiIP. + return wifi_ip + + def GetSubscriberInfo(self): + """Returns the device subscriber info (e.g. GSM and device ID) as string.""" + iphone_sub = self.RunShellCommand('dumpsys iphonesubinfo') + # Do not assert here. Devices (e.g. Nakasi on K) may not have iphonesubinfo. + return '\n'.join(iphone_sub) + + def GetBatteryInfo(self): + """Returns a {str: str} dict of battery info (e.g. status, level, etc).""" + battery = self.RunShellCommand('dumpsys battery') + assert battery + battery_info = {} + for line in battery[1:]: + k, _, v = line.partition(': ') + battery_info[k.strip()] = v.strip() + return battery_info + + def GetSetupWizardStatus(self): + """Returns the status of the device setup wizard (e.g. DISABLED).""" + status = self.system_properties['ro.setupwizard.mode'] + # On some devices, the status is empty if not otherwise set. In such cases + # the caller should expect an empty string to be returned. + return status + + def StartMonitoringLogcat(self, clear=True, logfile=None, filters=None): + """Starts monitoring the output of logcat, for use with WaitForLogMatch. + + Args: + clear: If True the existing logcat output will be cleared, to avoiding + matching historical output lurking in the log. + filters: A list of logcat filters to be used. + """ + if clear: + self.RunShellCommand('logcat -c') + args = [] + if self._adb._target_arg: + args += shlex.split(self._adb._target_arg) + args += ['logcat', '-v', 'threadtime'] + if filters: + args.extend(filters) + else: + args.append('*:v') + + if logfile: + logfile = NewLineNormalizer(logfile) + + # Spawn logcat and synchronize with it. + for _ in range(4): + self._logcat = pexpect.spawn(constants.GetAdbPath(), args, timeout=10, + logfile=logfile) + if not clear or self.SyncLogCat(): + break + self._logcat.close(force=True) + else: + logging.critical('Error reading from logcat: ' + str(self._logcat.match)) + sys.exit(1) + + def SyncLogCat(self): + """Synchronize with logcat. + + Synchronize with the monitored logcat so that WaitForLogMatch will only + consider new message that are received after this point in time. + + Returns: + True if the synchronization succeeded. + """ + assert self._logcat + tag = 'logcat_sync_%s' % time.time() + self.RunShellCommand('log ' + tag) + return self._logcat.expect([tag, pexpect.EOF, pexpect.TIMEOUT]) == 0 + + def GetMonitoredLogCat(self): + """Returns an "adb logcat" command as created by pexpected.spawn.""" + if not self._logcat: + self.StartMonitoringLogcat(clear=False) + return self._logcat + + def WaitForLogMatch(self, success_re, error_re, clear=False, timeout=10): + """Blocks until a matching line is logged or a timeout occurs. + + Args: + success_re: A compiled re to search each line for. + error_re: A compiled re which, if found, terminates the search for + |success_re|. If None is given, no error condition will be detected. + clear: If True the existing logcat output will be cleared, defaults to + false. + timeout: Timeout in seconds to wait for a log match. + + Raises: + pexpect.TIMEOUT after |timeout| seconds without a match for |success_re| + or |error_re|. + + Returns: + The re match object if |success_re| is matched first or None if |error_re| + is matched first. + """ + logging.info('<<< Waiting for logcat:' + str(success_re.pattern)) + t0 = time.time() + while True: + if not self._logcat: + self.StartMonitoringLogcat(clear) + try: + while True: + # Note this will block for upto the timeout _per log line_, so we need + # to calculate the overall timeout remaining since t0. + time_remaining = t0 + timeout - time.time() + if time_remaining < 0: + raise pexpect.TIMEOUT(self._logcat) + self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining) + line = self._logcat.match.group(1) + if error_re: + error_match = error_re.search(line) + if error_match: + return None + success_match = success_re.search(line) + if success_match: + return success_match + logging.info('<<< Skipped Logcat Line:' + str(line)) + except pexpect.TIMEOUT: + raise pexpect.TIMEOUT( + 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv ' + 'to debug)' % + (timeout, success_re.pattern)) + except pexpect.EOF: + # It seems that sometimes logcat can end unexpectedly. This seems + # to happen during Chrome startup after a reboot followed by a cache + # clean. I don't understand why this happens, but this code deals with + # getting EOF in logcat. + logging.critical('Found EOF in adb logcat. Restarting...') + # Rerun spawn with original arguments. Note that self._logcat.args[0] is + # the path of adb, so we don't want it in the arguments. + self._logcat = pexpect.spawn(constants.GetAdbPath(), + self._logcat.args[1:], + timeout=self._logcat.timeout, + logfile=self._logcat.logfile) + + def StartRecordingLogcat(self, clear=True, filters=None): + """Starts recording logcat output to eventually be saved as a string. + + This call should come before some series of tests are run, with either + StopRecordingLogcat or SearchLogcatRecord following the tests. + + Args: + clear: True if existing log output should be cleared. + filters: A list of logcat filters to be used. + """ + if not filters: + filters = ['*:v'] + if clear: + self._adb.SendCommand('logcat -c') + logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg, + ' '.join(filters)) + self._logcat_tmpoutfile = tempfile.NamedTemporaryFile(bufsize=0) + self.logcat_process = subprocess.Popen(logcat_command, shell=True, + stdout=self._logcat_tmpoutfile) + + def GetCurrentRecordedLogcat(self): + """Return the current content of the logcat being recorded. + Call this after StartRecordingLogcat() and before StopRecordingLogcat(). + This can be useful to perform timed polling/parsing. + Returns: + Current logcat output as a single string, or None if + StopRecordingLogcat() was already called. + """ + if not self._logcat_tmpoutfile: + return None + + with open(self._logcat_tmpoutfile.name) as f: + return f.read() + + def StopRecordingLogcat(self): + """Stops an existing logcat recording subprocess and returns output. + + Returns: + The logcat output as a string or an empty string if logcat was not + being recorded at the time. + """ + if not self.logcat_process: + return '' + # Cannot evaluate directly as 0 is a possible value. + # Better to read the self.logcat_process.stdout before killing it, + # Otherwise the communicate may return incomplete output due to pipe break. + if self.logcat_process.poll() is None: + self.logcat_process.kill() + self.logcat_process.wait() + self.logcat_process = None + self._logcat_tmpoutfile.seek(0) + output = self._logcat_tmpoutfile.read() + self._logcat_tmpoutfile.close() + self._logcat_tmpoutfile = None + return output + + @staticmethod + def SearchLogcatRecord(record, message, thread_id=None, proc_id=None, + log_level=None, component=None): + """Searches the specified logcat output and returns results. + + This method searches through the logcat output specified by record for a + certain message, narrowing results by matching them against any other + specified criteria. It returns all matching lines as described below. + + Args: + record: A string generated by Start/StopRecordingLogcat to search. + message: An output string to search for. + thread_id: The thread id that is the origin of the message. + proc_id: The process that is the origin of the message. + log_level: The log level of the message. + component: The name of the component that would create the message. + + Returns: + A list of dictionaries represeting matching entries, each containing keys + thread_id, proc_id, log_level, component, and message. + """ + if thread_id: + thread_id = str(thread_id) + if proc_id: + proc_id = str(proc_id) + results = [] + reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$', + re.MULTILINE) + log_list = reg.findall(record) + for (tid, pid, log_lev, comp, msg) in log_list: + if ((not thread_id or thread_id == tid) and + (not proc_id or proc_id == pid) and + (not log_level or log_level == log_lev) and + (not component or component == comp) and msg.find(message) > -1): + match = dict({'thread_id': tid, 'proc_id': pid, + 'log_level': log_lev, 'component': comp, + 'message': msg}) + results.append(match) + return results + + def ExtractPid(self, process_name): + """Extracts Process Ids for a given process name from Android Shell. + + Args: + process_name: name of the process on the device. + + Returns: + List of all the process ids (as strings) that match the given name. + If the name of a process exactly matches the given name, the pid of + that process will be inserted to the front of the pid list. + """ + pids = [] + for line in self.RunShellCommand('ps', log_result=False): + data = line.split() + try: + if process_name in data[-1]: # name is in the last column + if process_name == data[-1]: + pids.insert(0, data[1]) # PID is in the second column + else: + pids.append(data[1]) + except IndexError: + pass + return pids + + def GetIoStats(self): + """Gets cumulative disk IO stats since boot (for all processes). + + Returns: + Dict of {num_reads, num_writes, read_ms, write_ms} or None if there + was an error. + """ + IoStats = collections.namedtuple( + 'IoStats', + ['device', + 'num_reads_issued', + 'num_reads_merged', + 'num_sectors_read', + 'ms_spent_reading', + 'num_writes_completed', + 'num_writes_merged', + 'num_sectors_written', + 'ms_spent_writing', + 'num_ios_in_progress', + 'ms_spent_doing_io', + 'ms_spent_doing_io_weighted', + ]) + + for line in self.GetFileContents('/proc/diskstats', log_result=False): + fields = line.split() + stats = IoStats._make([fields[2]] + [int(f) for f in fields[3:]]) + if stats.device == 'mmcblk0': + return { + 'num_reads': stats.num_reads_issued, + 'num_writes': stats.num_writes_completed, + 'read_ms': stats.ms_spent_reading, + 'write_ms': stats.ms_spent_writing, + } + logging.warning('Could not find disk IO stats.') + return None + + def GetMemoryUsageForPid(self, pid): + """Returns the memory usage for given pid. + + Args: + pid: The pid number of the specific process running on device. + + Returns: + Dict of {metric:usage_kb}, for the process which has specified pid. + The metric keys which may be included are: Size, Rss, Pss, Shared_Clean, + Shared_Dirty, Private_Clean, Private_Dirty, VmHWM. + """ + showmap = self.RunShellCommand('showmap %d' % pid) + if not showmap or not showmap[-1].endswith('TOTAL'): + logging.warning('Invalid output for showmap %s', str(showmap)) + return {} + items = showmap[-1].split() + if len(items) != 9: + logging.warning('Invalid TOTAL for showmap %s', str(items)) + return {} + usage_dict = collections.defaultdict(int) + usage_dict.update({ + 'Size': int(items[0].strip()), + 'Rss': int(items[1].strip()), + 'Pss': int(items[2].strip()), + 'Shared_Clean': int(items[3].strip()), + 'Shared_Dirty': int(items[4].strip()), + 'Private_Clean': int(items[5].strip()), + 'Private_Dirty': int(items[6].strip()), + }) + peak_value_kb = 0 + for line in self.GetProtectedFileContents('/proc/%s/status' % pid): + if not line.startswith('VmHWM:'): # Format: 'VmHWM: +[0-9]+ kB' + continue + peak_value_kb = int(line.split(':')[1].strip().split(' ')[0]) + break + usage_dict['VmHWM'] = peak_value_kb + if not peak_value_kb: + logging.warning('Could not find memory peak value for pid ' + str(pid)) + + return usage_dict + + def ProcessesUsingDevicePort(self, device_port): + """Lists processes using the specified device port on loopback interface. + + Args: + device_port: Port on device we want to check. + + Returns: + A list of (pid, process_name) tuples using the specified port. + """ + tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False) + tcp_address = '0100007F:%04X' % device_port + pids = [] + for single_connect in tcp_results: + connect_results = single_connect.split() + # Column 1 is the TCP port, and Column 9 is the inode of the socket + if connect_results[1] == tcp_address: + socket_inode = connect_results[9] + socket_name = 'socket:[%s]' % socket_inode + lsof_results = self.RunShellCommand('lsof', log_result=False) + for single_process in lsof_results: + process_results = single_process.split() + # Ignore the line if it has less than nine columns in it, which may + # be the case when a process stops while lsof is executing. + if len(process_results) <= 8: + continue + # Column 0 is the executable name + # Column 1 is the pid + # Column 8 is the Inode in use + if process_results[8] == socket_name: + pids.append((int(process_results[1]), process_results[0])) + break + logging.info('PidsUsingDevicePort: %s', pids) + return pids + + def FileExistsOnDevice(self, file_name): + """Checks whether the given file exists on the device. + + Args: + file_name: Full path of file to check. + + Returns: + True if the file exists, False otherwise. + """ + assert '"' not in file_name, 'file_name cannot contain double quotes' + try: + status = self._adb.SendShellCommand( + '\'test -e "%s"; echo $?\'' % (file_name)) + if 'test: not found' not in status: + return int(status) == 0 + + status = self._adb.SendShellCommand( + '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name)) + return int(status) == 0 + except ValueError: + if IsDeviceAttached(self._device): + raise errors.DeviceUnresponsiveError('Device may be offline.') + + return False + + def IsFileWritableOnDevice(self, file_name): + """Checks whether the given file (or directory) is writable on the device. + + Args: + file_name: Full path of file/directory to check. + + Returns: + True if writable, False otherwise. + """ + assert '"' not in file_name, 'file_name cannot contain double quotes' + try: + status = self._adb.SendShellCommand( + '\'test -w "%s"; echo $?\'' % (file_name)) + if 'test: not found' not in status: + return int(status) == 0 + raise errors.AbortError('"test" binary not found. OS too old.') + + except ValueError: + if IsDeviceAttached(self._device): + raise errors.DeviceUnresponsiveError('Device may be offline.') + + return False + + @staticmethod + def GetTimestamp(): + return time.strftime('%Y-%m-%d-%H%M%S', time.localtime()) + + @staticmethod + def EnsureHostDirectory(host_file): + host_dir = os.path.dirname(os.path.abspath(host_file)) + if not os.path.exists(host_dir): + os.makedirs(host_dir) + + def TakeScreenshot(self, host_file=None): + """Saves a screenshot image to |host_file| on the host. + + Args: + host_file: Absolute path to the image file to store on the host or None to + use an autogenerated file name. + + Returns: + Resulting host file name of the screenshot. + """ + host_file = os.path.abspath(host_file or + 'screenshot-%s.png' % self.GetTimestamp()) + self.EnsureHostDirectory(host_file) + device_file = '%s/screenshot.png' % self.GetExternalStorage() + self.RunShellCommand( + '/system/bin/screencap -p %s' % device_file) + self.PullFileFromDevice(device_file, host_file) + self.RunShellCommand('rm -f "%s"' % device_file) + return host_file + + def PullFileFromDevice(self, device_file, host_file): + """Download |device_file| on the device from to |host_file| on the host. + + Args: + device_file: Absolute path to the file to retrieve from the device. + host_file: Absolute path to the file to store on the host. + """ + if not self._adb.Pull(device_file, host_file): + raise device_errors.AdbCommandFailedError( + ['pull', device_file, host_file], 'Failed to pull file from device.') + assert os.path.exists(host_file) + + def SetUtilWrapper(self, util_wrapper): + """Sets a wrapper prefix to be used when running a locally-built + binary on the device (ex.: md5sum_bin). + """ + self._util_wrapper = util_wrapper + + def RunUIAutomatorTest(self, test, test_package, timeout): + """Runs a single uiautomator test. + + Args: + test: Test class/method. + test_package: Name of the test jar. + timeout: Timeout time in seconds. + + Returns: + An instance of am_instrument_parser.TestResult object. + """ + cmd = 'uiautomator runtest %s -e class %s' % (test_package, test) + self._LogShell(cmd) + output = self._adb.SendShellCommand(cmd, timeout_time=timeout) + # uiautomator doesn't fully conform to the instrumenation test runner + # convention and doesn't terminate with INSTRUMENTATION_CODE. + # Just assume the first result is valid. + (test_results, _) = am_instrument_parser.ParseAmInstrumentOutput(output) + if not test_results: + raise errors.InstrumentationError( + 'no test results... device setup correctly?') + return test_results[0] + + def DismissCrashDialogIfNeeded(self): + """Dismiss the error/ANR dialog if present. + + Returns: Name of the crashed package if a dialog is focused, + None otherwise. + """ + re_focus = re.compile( + r'\s*mCurrentFocus.*Application (Error|Not Responding): (\S+)}') + + def _FindFocusedWindow(): + match = None + for line in self.RunShellCommand('dumpsys window windows'): + match = re.match(re_focus, line) + if match: + break + return match + + match = _FindFocusedWindow() + if not match: + return + package = match.group(2) + logging.warning('Trying to dismiss %s dialog for %s' % match.groups()) + self.SendKeyEvent(KEYCODE_DPAD_RIGHT) + self.SendKeyEvent(KEYCODE_DPAD_RIGHT) + self.SendKeyEvent(KEYCODE_ENTER) + match = _FindFocusedWindow() + if match: + logging.error('Still showing a %s dialog for %s' % match.groups()) + return package + + def EfficientDeviceDirectoryCopy(self, source, dest): + """ Copy a directory efficiently on the device + + Uses a shell script running on the target to copy new and changed files the + source directory to the destination directory and remove added files. This + is in some cases much faster than cp -r. + + Args: + source: absolute path of source directory + dest: absolute path of destination directory + """ + logging.info('In EfficientDeviceDirectoryCopy %s %s', source, dest) + with DeviceTempFile(self, suffix=".sh") as temp_script_file: + host_script_path = os.path.join(constants.DIR_SOURCE_ROOT, + 'build', + 'android', + 'pylib', + 'efficient_android_directory_copy.sh') + self._adb.Push(host_script_path, temp_script_file.name) + out = self.RunShellCommand( + 'sh %s %s %s' % (temp_script_file.name, source, dest), + timeout_time=120) + if self._device: + device_repr = self._device[-4:] + else: + device_repr = '????' + for line in out: + logging.info('[%s]> %s', device_repr, line) + + def _GetControlUsbChargingCommand(self): + if self._control_usb_charging_command['cached']: + return self._control_usb_charging_command['command'] + self._control_usb_charging_command['cached'] = True + if not self.IsRootEnabled(): + return None + for command in CONTROL_USB_CHARGING_COMMANDS: + # Assert command is valid. + assert 'disable_command' in command + assert 'enable_command' in command + assert 'witness_file' in command + witness_file = command['witness_file'] + if self.FileExistsOnDevice(witness_file): + self._control_usb_charging_command['command'] = command + return command + return None + + def CanControlUsbCharging(self): + return self._GetControlUsbChargingCommand() is not None + + def DisableUsbCharging(self, timeout=10): + command = self._GetControlUsbChargingCommand() + if not command: + raise Exception('Unable to act on usb charging.') + disable_command = command['disable_command'] + t0 = time.time() + # Do not loop directly on self.IsDeviceCharging to cut the number of calls + # to the device. + while True: + if t0 + timeout - time.time() < 0: + raise pexpect.TIMEOUT('Unable to disable USB charging in time: %s' % ( + self.GetBatteryInfo())) + self.RunShellCommand(disable_command) + if not self.IsDeviceCharging(): + break + + def EnableUsbCharging(self, timeout=10): + command = self._GetControlUsbChargingCommand() + if not command: + raise Exception('Unable to act on usb charging.') + disable_command = command['enable_command'] + t0 = time.time() + # Do not loop directly on self.IsDeviceCharging to cut the number of calls + # to the device. + while True: + if t0 + timeout - time.time() < 0: + raise pexpect.TIMEOUT('Unable to enable USB charging in time.') + self.RunShellCommand(disable_command) + if self.IsDeviceCharging(): + break + + def IsDeviceCharging(self): + for line in self.RunShellCommand('dumpsys battery'): + if 'powered: ' in line: + if line.split('powered: ')[1] == 'true': + return True + + +class NewLineNormalizer(object): + """A file-like object to normalize EOLs to '\n'. + + Pexpect runs adb within a pseudo-tty device (see + http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written + as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate + lines, the log ends up having '\r\r\n' at the end of each line. This + filter replaces the above with a single '\n' in the data stream. + """ + def __init__(self, output): + self._output = output + + def write(self, data): + data = data.replace('\r\r\n', '\n') + self._output.write(data) + + def flush(self): + self._output.flush() diff --git a/build/android/pylib/android_commands_unittest.py b/build/android/pylib/android_commands_unittest.py new file mode 100644 index 0000000..21c34f9 --- /dev/null +++ b/build/android/pylib/android_commands_unittest.py @@ -0,0 +1,191 @@ +# Copyright 2014 The Chromium 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 os +import shutil +import sys +import unittest + +sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) + +from pylib import android_commands + +# pylint: disable=W0212,W0702 + +class TestDeviceTempFile(unittest.TestCase): + def setUp(self): + if not os.getenv('BUILDTYPE'): + os.environ['BUILDTYPE'] = 'Debug' + + devices = android_commands.GetAttachedDevices() + self.assertGreater(len(devices), 0, 'No device attached!') + self.ac = android_commands.AndroidCommands(device=devices[0]) + + def testTempFileDeleted(self): + """Tests that DeviceTempFile deletes files when closed.""" + temp_file = android_commands.DeviceTempFile(self.ac) + self.assertFalse(self.ac.FileExistsOnDevice(temp_file.name)) + self.ac.SetFileContents(temp_file.name, "contents") + self.assertTrue(self.ac.FileExistsOnDevice(temp_file.name)) + temp_file.close() + self.assertFalse(self.ac.FileExistsOnDevice(temp_file.name)) + + with android_commands.DeviceTempFile(self.ac) as with_temp_file: + self.assertFalse(self.ac.FileExistsOnDevice(with_temp_file.name)) + self.ac.SetFileContents(with_temp_file.name, "contents") + self.assertTrue(self.ac.FileExistsOnDevice(with_temp_file.name)) + + self.assertFalse(self.ac.FileExistsOnDevice(with_temp_file.name)) + + def testTempFileNotWritten(self): + """Tests that device temp files work successfully even if not written to.""" + temp_file = android_commands.DeviceTempFile(self.ac) + temp_file.close() + self.assertFalse(self.ac.FileExistsOnDevice(temp_file.name)) + + with android_commands.DeviceTempFile(self.ac) as with_temp_file: + pass + self.assertFalse(self.ac.FileExistsOnDevice(with_temp_file.name)) + + def testNaming(self): + """Tests that returned filenames are as requested.""" + temp_file = android_commands.DeviceTempFile(self.ac, prefix="cat") + self.assertTrue(os.path.basename(temp_file.name).startswith("cat")) + + temp_file = android_commands.DeviceTempFile(self.ac, suffix="dog") + self.assertTrue(temp_file.name.endswith("dog")) + + temp_file = android_commands.DeviceTempFile( + self.ac, prefix="cat", suffix="dog") + self.assertTrue(os.path.basename(temp_file.name).startswith("cat")) + self.assertTrue(temp_file.name.endswith("dog")) + + +class TestGetFilesChanged(unittest.TestCase): + + def setUp(self): + if not os.getenv('BUILDTYPE'): + os.environ['BUILDTYPE'] = 'Debug' + + devices = android_commands.GetAttachedDevices() + self.assertGreater(len(devices), 0, 'No device attached!') + self.ac = android_commands.AndroidCommands(device=devices[0]) + self.host_data_dir = os.path.realpath('test_push_data') + self.device_data_dir = '%s/test_push_data' % ( + self.ac.RunShellCommand('realpath %s' % + self.ac.GetExternalStorage())[0]) + + os.mkdir(self.host_data_dir) + for i in xrange(1, 10): + with open('%s/%d.txt' % (self.host_data_dir, i), 'w') as f: + f.write('file #%d' % i) + + self.ac.RunShellCommand('mkdir %s' % self.device_data_dir) + + def testGetFilesChangedAllNeeded(self): + """ Tests GetFilesChanged when none of the files are on the device. + """ + expected = [('%s/%d.txt' % (self.host_data_dir, i), + '%s/%d.txt' % (self.device_data_dir, i)) + for i in xrange(1, 10)] + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def testGetFilesChangedSomeIdentical(self): + """ Tests GetFilesChanged when some of the files are on the device. + """ + for i in xrange(1, 5): + self.ac._adb.Push('%s/%d.txt' % (self.host_data_dir, i), + self.device_data_dir) + expected = [('%s/%d.txt' % (self.host_data_dir, i), + '%s/%d.txt' % (self.device_data_dir, i)) + for i in xrange(5, 10)] + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def testGetFilesChangedAllIdentical(self): + """ Tests GetFilesChanged when all of the files are on the device. + """ + for i in xrange(1, 10): + self.ac._adb.Push('%s/%d.txt' % (self.host_data_dir, i), + self.device_data_dir) + expected = [] + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def testGetFilesChangedRename(self): + """ Tests GetFilesChanged when one of the files has been renamed. + + This tests both with and without the ignore_filenames flag set. + """ + for i in xrange(5, 10): + self.ac._adb.Push('%s/%d.txt' % (self.host_data_dir, i), + self.device_data_dir) + os.rename('%s/5.txt' % (self.host_data_dir), + '%s/99.txt' % (self.host_data_dir)) + + expected = [('%s/%d.txt' % (self.host_data_dir, i), + '%s/%d.txt' % (self.device_data_dir, i)) + for i in xrange(1, 5)] + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir, + ignore_filenames=True) + self.assertSequenceEqual(expected, actual) + + expected.append(('%s/99.txt' % self.host_data_dir, + '%s/99.txt' % self.device_data_dir)) + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def testGetFilesChangedCopy(self): + """ Tests GetFilesChanged when one of the files has been copied. + + This tests both with and without the ignore_filenames flag set. + """ + for i in xrange(5, 10): + self.ac._adb.Push('%s/%d.txt' % (self.host_data_dir, i), + self.device_data_dir) + shutil.copy('%s/5.txt' % self.host_data_dir, + '%s/99.txt' % self.host_data_dir) + + expected = [('%s/%d.txt' % (self.host_data_dir, i), + '%s/%d.txt' % (self.device_data_dir, i)) + for i in xrange(1, 5)] + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir, + ignore_filenames=True) + self.assertSequenceEqual(expected, actual) + + expected.append(('%s/99.txt' % self.host_data_dir, + '%s/99.txt' % self.device_data_dir)) + actual = self.ac.GetFilesChanged(self.host_data_dir, self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def testGetFilesChangedIndividual(self): + """ Tests GetFilesChanged when provided one file. + """ + expected = [('%s/1.txt' % self.host_data_dir, + '%s/1.txt' % self.device_data_dir)] + actual = self.ac.GetFilesChanged('%s/1.txt' % self.host_data_dir, + '%s/1.txt' % self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def testGetFilesChangedFileToDirectory(self): + """ Tests GetFilesChanged when provided a file from the host and a + directory on the device. + """ + expected = [('%s/1.txt' % self.host_data_dir, + '%s' % self.device_data_dir)] + actual = self.ac.GetFilesChanged('%s/1.txt' % self.host_data_dir, + '%s' % self.device_data_dir) + self.assertSequenceEqual(expected, actual) + + def tearDown(self): + try: + shutil.rmtree(self.host_data_dir) + self.ac.RunShellCommand('rm -rf %s' % self.device_data_dir) + except: + pass + +if __name__ == '__main__': + unittest.main() + diff --git a/build/android/pylib/device/battery_utils_test.py b/build/android/pylib/device/battery_utils_test.py index bc1e341..3032b0d 100755 --- a/build/android/pylib/device/battery_utils_test.py +++ b/build/android/pylib/device/battery_utils_test.py @@ -21,6 +21,11 @@ from pylib.device import device_utils from pylib.device import device_utils_test from pylib.utils import mock_calls +# RunCommand from third_party/android_testrunner/run_command.py is mocked +# below, so its path needs to be in sys.path. +sys.path.append(os.path.join( + constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner')) + sys.path.append(os.path.join( constants.DIR_SOURCE_ROOT, 'third_party', 'pymock')) import mock # pylint: disable=F0401 diff --git a/build/android/pylib/device/decorators.py b/build/android/pylib/device/decorators.py index ca29eb3..73c13da 100644 --- a/build/android/pylib/device/decorators.py +++ b/build/android/pylib/device/decorators.py @@ -17,6 +17,12 @@ from pylib.device import device_errors from pylib.utils import reraiser_thread from pylib.utils import timeout_retry +# TODO(jbudorick) Remove once the DeviceUtils implementations are no longer +# backed by AndroidCommands / android_testrunner. +sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', + 'android_testrunner')) +import errors as old_errors + DEFAULT_TIMEOUT_ATTR = '_default_timeout' DEFAULT_RETRIES_ATTR = '_default_retries' @@ -49,6 +55,12 @@ def _TimeoutRetryWrapper(f, timeout_func, retries_func, pass_values=False): return impl() else: return timeout_retry.Run(impl, timeout, retries) + except old_errors.WaitForResponseTimedOutError as e: + raise device_errors.CommandTimeoutError(str(e)), None, ( + sys.exc_info()[2]) + except old_errors.DeviceUnresponsiveError as e: + raise device_errors.DeviceUnreachableError(str(e)), None, ( + sys.exc_info()[2]) except reraiser_thread.TimeoutError as e: raise device_errors.CommandTimeoutError(str(e)), None, ( sys.exc_info()[2]) diff --git a/build/android/pylib/device/decorators_test.py b/build/android/pylib/device/decorators_test.py index ec167ae..b75618b 100644 --- a/build/android/pylib/device/decorators_test.py +++ b/build/android/pylib/device/decorators_test.py @@ -19,6 +19,12 @@ from pylib.device import decorators from pylib.device import device_errors from pylib.utils import reraiser_thread +# TODO(jbudorick) Remove once the DeviceUtils implementations are no longer +# backed by AndroidCommands / android_testrunner. +sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', + 'android_testrunner')) +import errors as old_errors + _DEFAULT_TIMEOUT = 30 _DEFAULT_RETRIES = 3 @@ -72,6 +78,26 @@ class DecoratorsTest(unittest.TestCase): self.assertEquals(expected_timeout, actual_timeout) self.assertEquals(expected_retries, actual_retries) + def testFunctionDecoratorTranslatesOldExceptions(self): + """Tests that the explicit decorator translates old exceptions.""" + @decorators.WithTimeoutAndRetries + def alwaysRaisesProvidedException(exception, timeout=None, retries=None): + raise exception + + exception_desc = 'Old response timeout error' + with self.assertRaises(device_errors.CommandTimeoutError) as e: + alwaysRaisesProvidedException( + old_errors.WaitForResponseTimedOutError(exception_desc), + timeout=10, retries=1) + self.assertEquals(exception_desc, str(e.exception)) + + exception_desc = 'Old device error' + with self.assertRaises(device_errors.DeviceUnreachableError) as e: + alwaysRaisesProvidedException( + old_errors.DeviceUnresponsiveError(exception_desc), + timeout=10, retries=1) + self.assertEquals(exception_desc, str(e.exception)) + def testFunctionDecoratorTranslatesReraiserExceptions(self): """Tests that the explicit decorator translates reraiser exceptions.""" @decorators.WithTimeoutAndRetries @@ -140,6 +166,24 @@ class DecoratorsTest(unittest.TestCase): self.assertEquals(10, alwaysReturnsRetries()) self.assertEquals(1, alwaysReturnsRetries(retries=1)) + def testDefaultsFunctionDecoratorTranslatesOldExceptions(self): + """Tests that the explicit decorator translates old exceptions.""" + @decorators.WithTimeoutAndRetriesDefaults(30, 10) + def alwaysRaisesProvidedException(exception, timeout=None, retries=None): + raise exception + + exception_desc = 'Old response timeout error' + with self.assertRaises(device_errors.CommandTimeoutError) as e: + alwaysRaisesProvidedException( + old_errors.WaitForResponseTimedOutError(exception_desc)) + self.assertEquals(exception_desc, str(e.exception)) + + exception_desc = 'Old device error' + with self.assertRaises(device_errors.DeviceUnreachableError) as e: + alwaysRaisesProvidedException( + old_errors.DeviceUnresponsiveError(exception_desc)) + self.assertEquals(exception_desc, str(e.exception)) + def testDefaultsFunctionDecoratorTranslatesReraiserExceptions(self): """Tests that the explicit decorator translates reraiser exceptions.""" @decorators.WithTimeoutAndRetriesDefaults(30, 10) @@ -179,6 +223,24 @@ class DecoratorsTest(unittest.TestCase): alwaysRaisesCommandFailedError() self.assertEquals(11, DecoratorsTest._decorated_function_called_count) + def testExplicitDecoratorTranslatesOldExceptions(self): + """Tests that the explicit decorator translates old exceptions.""" + @decorators.WithExplicitTimeoutAndRetries(30, 10) + def alwaysRaisesProvidedException(exception): + raise exception + + exception_desc = 'Old response timeout error' + with self.assertRaises(device_errors.CommandTimeoutError) as e: + alwaysRaisesProvidedException( + old_errors.WaitForResponseTimedOutError(exception_desc)) + self.assertEquals(exception_desc, str(e.exception)) + + exception_desc = 'Old device error' + with self.assertRaises(device_errors.DeviceUnreachableError) as e: + alwaysRaisesProvidedException( + old_errors.DeviceUnresponsiveError(exception_desc)) + self.assertEquals(exception_desc, str(e.exception)) + def testExplicitDecoratorTranslatesReraiserExceptions(self): """Tests that the explicit decorator translates reraiser exceptions.""" @decorators.WithExplicitTimeoutAndRetries(30, 10) @@ -274,6 +336,21 @@ class DecoratorsTest(unittest.TestCase): self.assertEquals(31, test_obj.alwaysReturnsRetries()) self.assertEquals(32, test_obj.alwaysReturnsRetries(retries=32)) + def testMethodDecoratorTranslatesOldExceptions(self): + test_obj = self._MethodDecoratorTestObject(self) + + exception_desc = 'Old response timeout error' + with self.assertRaises(device_errors.CommandTimeoutError) as e: + test_obj.alwaysRaisesProvidedException( + old_errors.WaitForResponseTimedOutError(exception_desc)) + self.assertEquals(exception_desc, str(e.exception)) + + exception_desc = 'Old device error' + with self.assertRaises(device_errors.DeviceUnreachableError) as e: + test_obj.alwaysRaisesProvidedException( + old_errors.DeviceUnresponsiveError(exception_desc)) + self.assertEquals(exception_desc, str(e.exception)) + def testMethodDecoratorTranslatesReraiserExceptions(self): test_obj = self._MethodDecoratorTestObject(self) diff --git a/build/android/pylib/device/device_utils.py b/build/android/pylib/device/device_utils.py index bc1f2ab..ae28cd5 100644 --- a/build/android/pylib/device/device_utils.py +++ b/build/android/pylib/device/device_utils.py @@ -23,6 +23,7 @@ import threading import time import zipfile +import pylib.android_commands from pylib import cmd_helper from pylib import constants from pylib import device_signal @@ -173,10 +174,16 @@ class DeviceUtils(object): value is provided. """ self.adb = None + self.old_interface = None if isinstance(device, basestring): self.adb = adb_wrapper.AdbWrapper(device) + self.old_interface = pylib.android_commands.AndroidCommands(device) elif isinstance(device, adb_wrapper.AdbWrapper): self.adb = device + self.old_interface = pylib.android_commands.AndroidCommands(str(device)) + elif isinstance(device, pylib.android_commands.AndroidCommands): + self.adb = adb_wrapper.AdbWrapper(device.GetDevice()) + self.old_interface = device else: raise ValueError('Unsupported device value: %r' % device) self._commands_installed = None diff --git a/build/android/pylib/device/device_utils_test.py b/build/android/pylib/device/device_utils_test.py index 580794d..1d5f9ef 100755 --- a/build/android/pylib/device/device_utils_test.py +++ b/build/android/pylib/device/device_utils_test.py @@ -19,6 +19,7 @@ import re import sys import unittest +from pylib import android_commands from pylib import cmd_helper from pylib import constants from pylib import device_signal @@ -29,6 +30,11 @@ from pylib.device import intent from pylib.sdk import split_select from pylib.utils import mock_calls +# RunCommand from third_party/android_testrunner/run_command.py is mocked +# below, so its path needs to be in sys.path. +sys.path.append(os.path.join( + constants.DIR_SOURCE_ROOT, 'third_party', 'android_testrunner')) + sys.path.append(os.path.join( constants.DIR_SOURCE_ROOT, 'third_party', 'pymock')) import mock # pylint: disable=F0401 @@ -52,6 +58,12 @@ class DeviceUtilsInitTest(unittest.TestCase): d = device_utils.DeviceUtils(a) self.assertEqual(serial, d.adb.GetDeviceSerial()) + def testInitWithAndroidCommands(self): + serial = '0fedcba987654321' + a = android_commands.AndroidCommands(device=serial) + d = device_utils.DeviceUtils(a) + self.assertEqual(serial, d.adb.GetDeviceSerial()) + def testInitWithMissing_fails(self): with self.assertRaises(ValueError): device_utils.DeviceUtils(None) diff --git a/build/android/pylib/gtest/test_package_apk.py b/build/android/pylib/gtest/test_package_apk.py index 1bf3913..b67cef1 100644 --- a/build/android/pylib/gtest/test_package_apk.py +++ b/build/android/pylib/gtest/test_package_apk.py @@ -14,6 +14,7 @@ import sys import tempfile import time +from pylib import android_commands from pylib import constants from pylib import pexpect from pylib.device import device_errors @@ -70,7 +71,8 @@ class TestPackageApk(TestPackage): else: raise device_errors.DeviceUnreachableError( 'Unable to find fifo on device %s ' % self._GetFifo()) - args = ['-s', device.adb.GetDeviceSerial(), 'shell', 'cat', self._GetFifo()] + args = shlex.split(device.old_interface.Adb()._target_arg) + args += ['shell', 'cat', self._GetFifo()] return pexpect.spawn('adb', args, timeout=timeout, logfile=logfile) def _StartActivity(self, device, force_stop=True): @@ -141,20 +143,9 @@ class TestPackageApk(TestPackage): self._StartActivity(device, force_stop=False) finally: self.tool.CleanUpEnvironment() - logfile = self._NewLineNormalizer(sys.stdout) + logfile = android_commands.NewLineNormalizer(sys.stdout) return self._WatchFifo(device, timeout=10, logfile=logfile) - class _NewLineNormalizer(object): - def __init__(self, output): - self._output = output - - def write(self, data): - data = data.replace('\r\r\n', '\n') - self._output.write(data) - - def flush(self): - self._output.flush() - #override def Install(self, device): self.tool.CopyFiles(device) diff --git a/build/android/pylib/instrumentation/test_runner.py b/build/android/pylib/instrumentation/test_runner.py index 8731eb1..0f2e53f 100644 --- a/build/android/pylib/instrumentation/test_runner.py +++ b/build/android/pylib/instrumentation/test_runner.py @@ -96,6 +96,8 @@ class TestRunner(base_test_runner.BaseTestRunner): str(self.device)) else: if self.device.SetJavaAsserts(self.options.set_asserts): + # TODO(jbudorick) How to best do shell restart after the + # android_commands refactor? self.device.RunShellCommand('stop') self.device.RunShellCommand('start') self.device.WaitUntilFullyBooted() diff --git a/build/android/pylib/system_properties.py b/build/android/pylib/system_properties.py new file mode 100644 index 0000000..3f16f86 --- /dev/null +++ b/build/android/pylib/system_properties.py @@ -0,0 +1,40 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + + +class SystemProperties(dict): + + """A dict interface to interact with device system properties. + + System properties are key/value pairs as exposed by adb shell getprop/setprop. + + This implementation minimizes interaction with the physical device. It is + valid for the lifetime of a boot. + """ + + def __init__(self, android_commands): + super(SystemProperties, self).__init__() + self._adb = android_commands + self._cached_static_properties = {} + + def __getitem__(self, key): + if self._IsStatic(key): + if key not in self._cached_static_properties: + self._cached_static_properties[key] = self._GetProperty(key) + return self._cached_static_properties[key] + return self._GetProperty(key) + + def __setitem__(self, key, value): + # TODO(tonyg): This can fail with no root. Verify that it succeeds. + self._adb.SendShellCommand('setprop %s "%s"' % (key, value), retry_count=3) + + @staticmethod + def _IsStatic(key): + # TODO(tonyg): This list is conservative and could be expanded as needed. + return (key.startswith('ro.boot.') or + key.startswith('ro.build.') or + key.startswith('ro.product.')) + + def _GetProperty(self, key): + return self._adb.SendShellCommand('getprop %s' % key, retry_count=3).strip() diff --git a/build/android/pylib/valgrind_tools.py b/build/android/pylib/valgrind_tools.py index 8e34adf..99719d0 100644 --- a/build/android/pylib/valgrind_tools.py +++ b/build/android/pylib/valgrind_tools.py @@ -103,6 +103,10 @@ class AddressSanitizerTool(BaseTool): def __init__(self, device): super(AddressSanitizerTool, self).__init__() self._device = device + # Configure AndroidCommands to run utils (such as md5sum_bin) under ASan. + # This is required because ASan is a compiler-based tool, and md5sum + # includes instrumented code from base. + device.old_interface.SetUtilWrapper(self.GetUtilWrapper()) @classmethod def CopyFiles(cls, device): |