[Android] Move some pylib modules into devil/
BUG=476719 TBR=maruel@chromium.org Review URL: https://codereview.chromium.org/1314913009 Cr-Original-Commit-Position: refs/heads/master@{#346806} Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src Cr-Mirrored-Commit: bb6ecf6b1c2f7e879db2213b0fcb494fba559110
This commit is contained in:
Родитель
c3e27fd223
Коммит
295f4dabaf
|
@ -41,15 +41,15 @@ def CommonChecks(input_api, output_api):
|
|||
output_api,
|
||||
unit_tests=[
|
||||
J('.', 'emma_coverage_stats_test.py'),
|
||||
J('devil', 'android', 'battery_utils_test.py'),
|
||||
J('devil', 'android', 'device_utils_test.py'),
|
||||
J('devil', 'android', 'md5sum_test.py'),
|
||||
J('devil', 'android', 'logcat_monitor_test.py'),
|
||||
J('pylib', 'base', 'test_dispatcher_unittest.py'),
|
||||
J('pylib', 'device', 'battery_utils_test.py'),
|
||||
J('pylib', 'device', 'device_utils_test.py'),
|
||||
J('pylib', 'device', 'logcat_monitor_test.py'),
|
||||
J('pylib', 'gtest', 'gtest_test_instance_test.py'),
|
||||
J('pylib', 'instrumentation',
|
||||
'instrumentation_test_instance_test.py'),
|
||||
J('pylib', 'results', 'json_results_test.py'),
|
||||
J('pylib', 'utils', 'md5sum_test.py'),
|
||||
],
|
||||
env=pylib_test_env))
|
||||
return output
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
jbudorick@chromium.org
|
||||
mikecase@chromium.org
|
||||
perezju@chromium.org
|
||||
rnephew@chromium.org
|
|
@ -0,0 +1,3 @@
|
|||
# Copyright 2015 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.
|
|
@ -0,0 +1,131 @@
|
|||
# Copyright (c) 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.
|
||||
|
||||
"""Module containing utilities for apk packages."""
|
||||
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from devil.android.sdk import aapt
|
||||
from devil.utils import cmd_helper
|
||||
from pylib import constants
|
||||
|
||||
|
||||
_AAPT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'aapt')
|
||||
_MANIFEST_ATTRIBUTE_RE = re.compile(
|
||||
r'\s*A: ([^\(\)= ]*)\([^\(\)= ]*\)="(.*)" \(Raw: .*\)$')
|
||||
_MANIFEST_ELEMENT_RE = re.compile(r'\s*(?:E|N): (\S*) .*$')
|
||||
_PACKAGE_NAME_RE = re.compile(r'package: .*name=\'(\S*)\'')
|
||||
_SPLIT_NAME_RE = re.compile(r'package: .*split=\'(\S*)\'')
|
||||
|
||||
|
||||
def GetPackageName(apk_path):
|
||||
"""Returns the package name of the apk."""
|
||||
return ApkHelper(apk_path).GetPackageName()
|
||||
|
||||
|
||||
# TODO(jbudorick): Deprecate and remove this function once callers have been
|
||||
# converted to ApkHelper.GetInstrumentationName
|
||||
def GetInstrumentationName(apk_path):
|
||||
"""Returns the name of the Instrumentation in the apk."""
|
||||
return ApkHelper(apk_path).GetInstrumentationName()
|
||||
|
||||
|
||||
def _ParseManifestFromApk(apk_path):
|
||||
aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml')
|
||||
|
||||
parsed_manifest = {}
|
||||
node_stack = [parsed_manifest]
|
||||
indent = ' '
|
||||
|
||||
for line in aapt_output[1:]:
|
||||
if len(line) == 0:
|
||||
continue
|
||||
|
||||
indent_depth = 0
|
||||
while line[(len(indent) * indent_depth):].startswith(indent):
|
||||
indent_depth += 1
|
||||
|
||||
node_stack = node_stack[:indent_depth]
|
||||
node = node_stack[-1]
|
||||
|
||||
m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:])
|
||||
if m:
|
||||
if not m.group(1) in node:
|
||||
node[m.group(1)] = {}
|
||||
node_stack += [node[m.group(1)]]
|
||||
continue
|
||||
|
||||
m = _MANIFEST_ATTRIBUTE_RE.match(line[len(indent) * indent_depth:])
|
||||
if m:
|
||||
if not m.group(1) in node:
|
||||
node[m.group(1)] = []
|
||||
node[m.group(1)].append(m.group(2))
|
||||
continue
|
||||
|
||||
return parsed_manifest
|
||||
|
||||
|
||||
class ApkHelper(object):
|
||||
def __init__(self, apk_path):
|
||||
self._apk_path = apk_path
|
||||
self._manifest = None
|
||||
self._package_name = None
|
||||
self._split_name = None
|
||||
|
||||
def GetActivityName(self):
|
||||
"""Returns the name of the Activity in the apk."""
|
||||
manifest_info = self._GetManifest()
|
||||
try:
|
||||
activity = (
|
||||
manifest_info['manifest']['application']['activity']
|
||||
['android:name'][0])
|
||||
except KeyError:
|
||||
return None
|
||||
if '.' not in activity:
|
||||
activity = '%s.%s' % (self.GetPackageName(), activity)
|
||||
elif activity.startswith('.'):
|
||||
activity = '%s%s' % (self.GetPackageName(), activity)
|
||||
return activity
|
||||
|
||||
def GetInstrumentationName(
|
||||
self, default='android.test.InstrumentationTestRunner'):
|
||||
"""Returns the name of the Instrumentation in the apk."""
|
||||
manifest_info = self._GetManifest()
|
||||
try:
|
||||
return manifest_info['manifest']['instrumentation']['android:name'][0]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def GetPackageName(self):
|
||||
"""Returns the package name of the apk."""
|
||||
if self._package_name:
|
||||
return self._package_name
|
||||
|
||||
aapt_output = aapt.Dump('badging', self._apk_path)
|
||||
for line in aapt_output:
|
||||
m = _PACKAGE_NAME_RE.match(line)
|
||||
if m:
|
||||
self._package_name = m.group(1)
|
||||
return self._package_name
|
||||
raise Exception('Failed to determine package name of %s' % self._apk_path)
|
||||
|
||||
def GetSplitName(self):
|
||||
"""Returns the name of the split of the apk."""
|
||||
if self._split_name:
|
||||
return self._split_name
|
||||
|
||||
aapt_output = aapt.Dump('badging', self._apk_path)
|
||||
for line in aapt_output:
|
||||
m = _SPLIT_NAME_RE.match(line)
|
||||
if m:
|
||||
self._split_name = m.group(1)
|
||||
return self._split_name
|
||||
return None
|
||||
|
||||
def _GetManifest(self):
|
||||
if not self._manifest:
|
||||
self._manifest = _ParseManifestFromApk(self._apk_path)
|
||||
return self._manifest
|
||||
|
|
@ -0,0 +1,635 @@
|
|||
# Copyright 2015 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 a variety of device interactions with power.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import csv
|
||||
import logging
|
||||
|
||||
from devil.android import decorators
|
||||
from devil.android import device_errors
|
||||
from devil.android import device_utils
|
||||
from devil.utils import timeout_retry
|
||||
from pylib import constants
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 3
|
||||
|
||||
|
||||
_DEVICE_PROFILES = [
|
||||
{
|
||||
'name': 'Nexus 4',
|
||||
'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
|
||||
'enable_command': (
|
||||
'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
|
||||
'dumpsys battery reset'),
|
||||
'disable_command': (
|
||||
'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': None,
|
||||
'voltage': None,
|
||||
'current': None,
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 5',
|
||||
# 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 && '
|
||||
'chmod 644 /sys/class/power_supply/usb/online && '
|
||||
'echo 1 > /sys/class/power_supply/usb/online && '
|
||||
'dumpsys battery reset'),
|
||||
'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 && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': None,
|
||||
'voltage': None,
|
||||
'current': None,
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 6',
|
||||
'witness_file': None,
|
||||
'enable_command': (
|
||||
'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
|
||||
'dumpsys battery reset'),
|
||||
'disable_command': (
|
||||
'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': (
|
||||
'/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
|
||||
'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
|
||||
'current': '/sys/class/power_supply/max170xx_battery/current_now',
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 9',
|
||||
'witness_file': None,
|
||||
'enable_command': (
|
||||
'echo Disconnected > '
|
||||
'/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
|
||||
'dumpsys battery reset'),
|
||||
'disable_command': (
|
||||
'echo Connected > '
|
||||
'/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': '/sys/class/power_supply/battery/charge_counter_ext',
|
||||
'voltage': '/sys/class/power_supply/battery/voltage_now',
|
||||
'current': '/sys/class/power_supply/battery/current_now',
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 10',
|
||||
'witness_file': None,
|
||||
'enable_command': None,
|
||||
'disable_command': None,
|
||||
'charge_counter': None,
|
||||
'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
|
||||
'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
# The list of useful dumpsys columns.
|
||||
# Index of the column containing the format version.
|
||||
_DUMP_VERSION_INDEX = 0
|
||||
# Index of the column containing the type of the row.
|
||||
_ROW_TYPE_INDEX = 3
|
||||
# Index of the column containing the uid.
|
||||
_PACKAGE_UID_INDEX = 4
|
||||
# Index of the column containing the application package.
|
||||
_PACKAGE_NAME_INDEX = 5
|
||||
# The column containing the uid of the power data.
|
||||
_PWI_UID_INDEX = 1
|
||||
# The column containing the type of consumption. Only consumption since last
|
||||
# charge are of interest here.
|
||||
_PWI_AGGREGATION_INDEX = 2
|
||||
_PWS_AGGREGATION_INDEX = _PWI_AGGREGATION_INDEX
|
||||
# The column containing the amount of power used, in mah.
|
||||
_PWI_POWER_CONSUMPTION_INDEX = 5
|
||||
_PWS_POWER_CONSUMPTION_INDEX = _PWI_POWER_CONSUMPTION_INDEX
|
||||
|
||||
|
||||
class BatteryUtils(object):
|
||||
|
||||
def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
|
||||
default_retries=_DEFAULT_RETRIES):
|
||||
"""BatteryUtils constructor.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils instance.
|
||||
default_timeout: An integer containing the default number of seconds to
|
||||
wait for an operation to complete if no explicit value
|
||||
is provided.
|
||||
default_retries: An integer containing the default number or times an
|
||||
operation should be retried on failure if no explicit
|
||||
value is provided.
|
||||
|
||||
Raises:
|
||||
TypeError: If it is not passed a DeviceUtils instance.
|
||||
"""
|
||||
if not isinstance(device, device_utils.DeviceUtils):
|
||||
raise TypeError('Must be initialized with DeviceUtils object.')
|
||||
self._device = device
|
||||
self._cache = device.GetClientCache(self.__class__.__name__)
|
||||
self._default_timeout = default_timeout
|
||||
self._default_retries = default_retries
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def SupportsFuelGauge(self, timeout=None, retries=None):
|
||||
"""Detect if fuel gauge chip is present.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
True if known fuel gauge files are present.
|
||||
False otherwise.
|
||||
"""
|
||||
self._DiscoverDeviceProfile()
|
||||
return (self._cache['profile']['enable_command'] != None
|
||||
and self._cache['profile']['charge_counter'] != None)
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
|
||||
"""Get value of charge_counter on fuel gauge chip.
|
||||
|
||||
Device must have charging disabled for this, not just battery updates
|
||||
disabled. The only device that this currently works with is the nexus 5.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
value of charge_counter for fuel gauge chip in units of nAh.
|
||||
|
||||
Raises:
|
||||
device_errors.CommandFailedError: If fuel gauge chip not found.
|
||||
"""
|
||||
if self.SupportsFuelGauge():
|
||||
return int(self._device.ReadFile(
|
||||
self._cache['profile']['charge_counter']))
|
||||
raise device_errors.CommandFailedError(
|
||||
'Unable to find fuel gauge.')
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetNetworkData(self, package, timeout=None, retries=None):
|
||||
"""Get network data for specific package.
|
||||
|
||||
Args:
|
||||
package: package name you want network data for.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
Tuple of (sent_data, recieved_data)
|
||||
None if no network data found
|
||||
"""
|
||||
# If device_utils clears cache, cache['uids'] doesn't exist
|
||||
if 'uids' not in self._cache:
|
||||
self._cache['uids'] = {}
|
||||
if package not in self._cache['uids']:
|
||||
self.GetPowerData()
|
||||
if package not in self._cache['uids']:
|
||||
logging.warning('No UID found for %s. Can\'t get network data.',
|
||||
package)
|
||||
return None
|
||||
|
||||
network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
|
||||
try:
|
||||
send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
|
||||
# If ReadFile throws exception, it means no network data usage file for
|
||||
# package has been recorded. Return 0 sent and 0 received.
|
||||
except device_errors.AdbShellCommandFailedError:
|
||||
logging.warning('No sent data found for package %s', package)
|
||||
send_data = 0
|
||||
try:
|
||||
recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
|
||||
except device_errors.AdbShellCommandFailedError:
|
||||
logging.warning('No received data found for package %s', package)
|
||||
recv_data = 0
|
||||
return (send_data, recv_data)
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetPowerData(self, timeout=None, retries=None):
|
||||
"""Get power data for device.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
Dict containing system power, and a per-package power dict keyed on
|
||||
package names.
|
||||
{
|
||||
'system_total': 23.1,
|
||||
'per_package' : {
|
||||
package_name: {
|
||||
'uid': uid,
|
||||
'data': [1,2,3]
|
||||
},
|
||||
}
|
||||
}
|
||||
"""
|
||||
if 'uids' not in self._cache:
|
||||
self._cache['uids'] = {}
|
||||
dumpsys_output = self._device.RunShellCommand(
|
||||
['dumpsys', 'batterystats', '-c'],
|
||||
check_return=True, large_output=True)
|
||||
csvreader = csv.reader(dumpsys_output)
|
||||
pwi_entries = collections.defaultdict(list)
|
||||
system_total = None
|
||||
for entry in csvreader:
|
||||
if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
|
||||
# Wrong dumpsys version.
|
||||
raise device_errors.DeviceVersionError(
|
||||
'Dumpsys version must be 8 or 9. %s found.'
|
||||
% entry[_DUMP_VERSION_INDEX])
|
||||
if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
|
||||
current_package = entry[_PACKAGE_NAME_INDEX]
|
||||
if (self._cache['uids'].get(current_package)
|
||||
and self._cache['uids'].get(current_package)
|
||||
!= entry[_PACKAGE_UID_INDEX]):
|
||||
raise device_errors.CommandFailedError(
|
||||
'Package %s found multiple times with different UIDs %s and %s'
|
||||
% (current_package, self._cache['uids'][current_package],
|
||||
entry[_PACKAGE_UID_INDEX]))
|
||||
self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
|
||||
elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
|
||||
and entry[_ROW_TYPE_INDEX] == 'pwi'
|
||||
and entry[_PWI_AGGREGATION_INDEX] == 'l'):
|
||||
pwi_entries[entry[_PWI_UID_INDEX]].append(
|
||||
float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
|
||||
elif (_PWS_POWER_CONSUMPTION_INDEX < len(entry)
|
||||
and entry[_ROW_TYPE_INDEX] == 'pws'
|
||||
and entry[_PWS_AGGREGATION_INDEX] == 'l'):
|
||||
# This entry should only appear once.
|
||||
assert system_total is None
|
||||
system_total = float(entry[_PWS_POWER_CONSUMPTION_INDEX])
|
||||
|
||||
per_package = {p: {'uid': uid, 'data': pwi_entries[uid]}
|
||||
for p, uid in self._cache['uids'].iteritems()}
|
||||
return {'system_total': system_total, 'per_package': per_package}
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetBatteryInfo(self, timeout=None, retries=None):
|
||||
"""Gets battery info for the device.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
Returns:
|
||||
A dict containing various battery information as reported by dumpsys
|
||||
battery.
|
||||
"""
|
||||
result = {}
|
||||
# Skip the first line, which is just a header.
|
||||
for line in self._device.RunShellCommand(
|
||||
['dumpsys', 'battery'], check_return=True)[1:]:
|
||||
# If usb charging has been disabled, an extra line of header exists.
|
||||
if 'UPDATES STOPPED' in line:
|
||||
logging.warning('Dumpsys battery not receiving updates. '
|
||||
'Run dumpsys battery reset if this is in error.')
|
||||
elif ':' not in line:
|
||||
logging.warning('Unknown line found in dumpsys battery: "%s"', line)
|
||||
else:
|
||||
k, v = line.split(':', 1)
|
||||
result[k.strip()] = v.strip()
|
||||
return result
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetCharging(self, timeout=None, retries=None):
|
||||
"""Gets the charging state of the device.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
Returns:
|
||||
True if the device is charging, false otherwise.
|
||||
"""
|
||||
battery_info = self.GetBatteryInfo()
|
||||
for k in ('AC powered', 'USB powered', 'Wireless powered'):
|
||||
if (k in battery_info and
|
||||
battery_info[k].lower() in ('true', '1', 'yes')):
|
||||
return True
|
||||
return False
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def SetCharging(self, enabled, timeout=None, retries=None):
|
||||
"""Enables or disables charging on the device.
|
||||
|
||||
Args:
|
||||
enabled: A boolean indicating whether charging should be enabled or
|
||||
disabled.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.CommandFailedError: If method of disabling charging cannot
|
||||
be determined.
|
||||
"""
|
||||
self._DiscoverDeviceProfile()
|
||||
if not self._cache['profile']['enable_command']:
|
||||
raise device_errors.CommandFailedError(
|
||||
'Unable to find charging commands.')
|
||||
|
||||
if enabled:
|
||||
command = self._cache['profile']['enable_command']
|
||||
else:
|
||||
command = self._cache['profile']['disable_command']
|
||||
|
||||
def verify_charging():
|
||||
return self.GetCharging() == enabled
|
||||
|
||||
self._device.RunShellCommand(
|
||||
command, check_return=True, as_root=True, large_output=True)
|
||||
timeout_retry.WaitFor(verify_charging, wait_period=1)
|
||||
|
||||
# TODO(rnephew): Make private when all use cases can use the context manager.
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def DisableBatteryUpdates(self, timeout=None, retries=None):
|
||||
"""Resets battery data and makes device appear like it is not
|
||||
charging so that it will collect power data since last charge.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.CommandFailedError: When resetting batterystats fails to
|
||||
reset power values.
|
||||
device_errors.DeviceVersionError: If device is not L or higher.
|
||||
"""
|
||||
def battery_updates_disabled():
|
||||
return self.GetCharging() is False
|
||||
|
||||
self._ClearPowerData()
|
||||
self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
|
||||
check_return=True)
|
||||
self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
|
||||
check_return=True)
|
||||
timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
|
||||
|
||||
# TODO(rnephew): Make private when all use cases can use the context manager.
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def EnableBatteryUpdates(self, timeout=None, retries=None):
|
||||
"""Restarts device charging so that dumpsys no longer collects power data.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.DeviceVersionError: If device is not L or higher.
|
||||
"""
|
||||
def battery_updates_enabled():
|
||||
return (self.GetCharging()
|
||||
or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
|
||||
['dumpsys', 'battery'], check_return=True)))
|
||||
|
||||
self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
|
||||
check_return=True)
|
||||
timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def BatteryMeasurement(self, timeout=None, retries=None):
|
||||
"""Context manager that enables battery data collection. It makes
|
||||
the device appear to stop charging so that dumpsys will start collecting
|
||||
power data since last charge. Once the with block is exited, charging is
|
||||
resumed and power data since last charge is no longer collected.
|
||||
|
||||
Only for devices L and higher.
|
||||
|
||||
Example usage:
|
||||
with BatteryMeasurement():
|
||||
browser_actions()
|
||||
get_power_data() # report usage within this block
|
||||
after_measurements() # Anything that runs after power
|
||||
# measurements are collected
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.DeviceVersionError: If device is not L or higher.
|
||||
"""
|
||||
if (self._device.build_version_sdk <
|
||||
constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
|
||||
raise device_errors.DeviceVersionError('Device must be L or higher.')
|
||||
try:
|
||||
self.DisableBatteryUpdates(timeout=timeout, retries=retries)
|
||||
yield
|
||||
finally:
|
||||
self.EnableBatteryUpdates(timeout=timeout, retries=retries)
|
||||
|
||||
def _DischargeDevice(self, percent, wait_period=120):
|
||||
"""Disables charging and waits for device to discharge given amount
|
||||
|
||||
Args:
|
||||
percent: level of charge to discharge.
|
||||
|
||||
Raises:
|
||||
ValueError: If percent is not between 1 and 99.
|
||||
"""
|
||||
battery_level = int(self.GetBatteryInfo().get('level'))
|
||||
if not 0 < percent < 100:
|
||||
raise ValueError('Discharge amount(%s) must be between 1 and 99'
|
||||
% percent)
|
||||
if battery_level is None:
|
||||
logging.warning('Unable to find current battery level. Cannot discharge.')
|
||||
return
|
||||
# Do not discharge if it would make battery level too low.
|
||||
if percent >= battery_level - 10:
|
||||
logging.warning('Battery is too low or discharge amount requested is too '
|
||||
'high. Cannot discharge phone %s percent.', percent)
|
||||
return
|
||||
|
||||
self.SetCharging(False)
|
||||
def device_discharged():
|
||||
self.SetCharging(True)
|
||||
current_level = int(self.GetBatteryInfo().get('level'))
|
||||
logging.info('current battery level: %s', current_level)
|
||||
if battery_level - current_level >= percent:
|
||||
return True
|
||||
self.SetCharging(False)
|
||||
return False
|
||||
|
||||
timeout_retry.WaitFor(device_discharged, wait_period=wait_period)
|
||||
|
||||
def ChargeDeviceToLevel(self, level, wait_period=60):
|
||||
"""Enables charging and waits for device to be charged to given level.
|
||||
|
||||
Args:
|
||||
level: level of charge to wait for.
|
||||
wait_period: time in seconds to wait between checking.
|
||||
"""
|
||||
self.SetCharging(True)
|
||||
|
||||
def device_charged():
|
||||
battery_level = self.GetBatteryInfo().get('level')
|
||||
if battery_level is None:
|
||||
logging.warning('Unable to find current battery level.')
|
||||
battery_level = 100
|
||||
else:
|
||||
logging.info('current battery level: %s', battery_level)
|
||||
battery_level = int(battery_level)
|
||||
return battery_level >= level
|
||||
|
||||
timeout_retry.WaitFor(device_charged, wait_period=wait_period)
|
||||
|
||||
def LetBatteryCoolToTemperature(self, target_temp, wait_period=180):
|
||||
"""Lets device sit to give battery time to cool down
|
||||
Args:
|
||||
temp: maximum temperature to allow in tenths of degrees c.
|
||||
wait_period: time in seconds to wait between checking.
|
||||
"""
|
||||
def cool_device():
|
||||
temp = self.GetBatteryInfo().get('temperature')
|
||||
if temp is None:
|
||||
logging.warning('Unable to find current battery temperature.')
|
||||
temp = 0
|
||||
else:
|
||||
logging.info('Current battery temperature: %s', temp)
|
||||
if int(temp) <= target_temp:
|
||||
return True
|
||||
else:
|
||||
if self._cache['profile']['name'] == 'Nexus 5':
|
||||
self._DischargeDevice(1)
|
||||
return False
|
||||
|
||||
self._DiscoverDeviceProfile()
|
||||
self.EnableBatteryUpdates()
|
||||
logging.info('Waiting for the device to cool down to %s (0.1 C)',
|
||||
target_temp)
|
||||
timeout_retry.WaitFor(cool_device, wait_period=wait_period)
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def TieredSetCharging(self, enabled, timeout=None, retries=None):
|
||||
"""Enables or disables charging on the device.
|
||||
|
||||
Args:
|
||||
enabled: A boolean indicating whether charging should be enabled or
|
||||
disabled.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
"""
|
||||
if self.GetCharging() == enabled:
|
||||
logging.warning('Device charging already in expected state: %s', enabled)
|
||||
return
|
||||
|
||||
self._DiscoverDeviceProfile()
|
||||
if enabled:
|
||||
if self._cache['profile']['enable_command']:
|
||||
self.SetCharging(enabled)
|
||||
else:
|
||||
logging.info('Unable to enable charging via hardware. '
|
||||
'Falling back to software enabling.')
|
||||
self.EnableBatteryUpdates()
|
||||
else:
|
||||
if self._cache['profile']['enable_command']:
|
||||
self._ClearPowerData()
|
||||
self.SetCharging(enabled)
|
||||
else:
|
||||
logging.info('Unable to disable charging via hardware. '
|
||||
'Falling back to software disabling.')
|
||||
self.DisableBatteryUpdates()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def PowerMeasurement(self, timeout=None, retries=None):
|
||||
"""Context manager that enables battery power collection.
|
||||
|
||||
Once the with block is exited, charging is resumed. Will attempt to disable
|
||||
charging at the hardware level, and if that fails will fall back to software
|
||||
disabling of battery updates.
|
||||
|
||||
Only for devices L and higher.
|
||||
|
||||
Example usage:
|
||||
with PowerMeasurement():
|
||||
browser_actions()
|
||||
get_power_data() # report usage within this block
|
||||
after_measurements() # Anything that runs after power
|
||||
# measurements are collected
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
"""
|
||||
try:
|
||||
self.TieredSetCharging(False, timeout=timeout, retries=retries)
|
||||
yield
|
||||
finally:
|
||||
self.TieredSetCharging(True, timeout=timeout, retries=retries)
|
||||
|
||||
def _ClearPowerData(self):
|
||||
"""Resets battery data and makes device appear like it is not
|
||||
charging so that it will collect power data since last charge.
|
||||
|
||||
Returns:
|
||||
True if power data cleared.
|
||||
False if power data clearing is not supported (pre-L)
|
||||
|
||||
Raises:
|
||||
device_errors.DeviceVersionError: If power clearing is supported,
|
||||
but fails.
|
||||
"""
|
||||
if (self._device.build_version_sdk <
|
||||
constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
|
||||
logging.warning('Dumpsys power data only available on 5.0 and above. '
|
||||
'Cannot clear power data.')
|
||||
return False
|
||||
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'batterystats', '--reset'], check_return=True)
|
||||
battery_data = self._device.RunShellCommand(
|
||||
['dumpsys', 'batterystats', '--charged', '-c'],
|
||||
check_return=True, large_output=True)
|
||||
for line in battery_data:
|
||||
l = line.split(',')
|
||||
if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi'
|
||||
and l[_PWI_POWER_CONSUMPTION_INDEX] != 0):
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'reset'], check_return=True)
|
||||
raise device_errors.CommandFailedError(
|
||||
'Non-zero pmi value found after reset.')
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'reset'], check_return=True)
|
||||
return True
|
||||
|
||||
def _DiscoverDeviceProfile(self):
|
||||
"""Checks and caches device information.
|
||||
|
||||
Returns:
|
||||
True if profile is found, false otherwise.
|
||||
"""
|
||||
|
||||
if 'profile' in self._cache:
|
||||
return True
|
||||
for profile in _DEVICE_PROFILES:
|
||||
if self._device.product_model == profile['name']:
|
||||
self._cache['profile'] = profile
|
||||
return True
|
||||
self._cache['profile'] = {
|
||||
'name': None,
|
||||
'witness_file': None,
|
||||
'enable_command': None,
|
||||
'disable_command': None,
|
||||
'charge_counter': None,
|
||||
'voltage': None,
|
||||
'current': None,
|
||||
}
|
||||
return False
|
|
@ -14,12 +14,12 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from devil.android import battery_utils
|
||||
from devil.android import device_errors
|
||||
from devil.android import device_utils
|
||||
from devil.android import device_utils_test
|
||||
from devil.utils import mock_calls
|
||||
from pylib import constants
|
||||
from pylib.device import battery_utils
|
||||
from pylib.device import device_errors
|
||||
from pylib.device import device_utils
|
||||
from pylib.device import device_utils_test
|
||||
from pylib.utils import mock_calls
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
|
@ -0,0 +1,145 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Function/method decorators that provide timeout and retry logic.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from devil.android import device_errors
|
||||
from devil.utils import cmd_helper
|
||||
from devil.utils import reraiser_thread
|
||||
from devil.utils import timeout_retry
|
||||
from pylib import constants
|
||||
|
||||
DEFAULT_TIMEOUT_ATTR = '_default_timeout'
|
||||
DEFAULT_RETRIES_ATTR = '_default_retries'
|
||||
|
||||
|
||||
def _TimeoutRetryWrapper(f, timeout_func, retries_func, pass_values=False):
|
||||
""" Wraps a funcion with timeout and retry handling logic.
|
||||
|
||||
Args:
|
||||
f: The function to wrap.
|
||||
timeout_func: A callable that returns the timeout value.
|
||||
retries_func: A callable that returns the retries value.
|
||||
pass_values: If True, passes the values returned by |timeout_func| and
|
||||
|retries_func| to the wrapped function as 'timeout' and
|
||||
'retries' kwargs, respectively.
|
||||
Returns:
|
||||
The wrapped function.
|
||||
"""
|
||||
@functools.wraps(f)
|
||||
def TimeoutRetryWrapper(*args, **kwargs):
|
||||
timeout = timeout_func(*args, **kwargs)
|
||||
retries = retries_func(*args, **kwargs)
|
||||
if pass_values:
|
||||
kwargs['timeout'] = timeout
|
||||
kwargs['retries'] = retries
|
||||
def impl():
|
||||
return f(*args, **kwargs)
|
||||
try:
|
||||
if isinstance(threading.current_thread(),
|
||||
timeout_retry.TimeoutRetryThread):
|
||||
return impl()
|
||||
else:
|
||||
return timeout_retry.Run(impl, timeout, retries)
|
||||
except reraiser_thread.TimeoutError as e:
|
||||
raise device_errors.CommandTimeoutError(str(e)), None, (
|
||||
sys.exc_info()[2])
|
||||
except cmd_helper.TimeoutError as e:
|
||||
raise device_errors.CommandTimeoutError(str(e)), None, (
|
||||
sys.exc_info()[2])
|
||||
return TimeoutRetryWrapper
|
||||
|
||||
|
||||
def WithTimeoutAndRetries(f):
|
||||
"""A decorator that handles timeouts and retries.
|
||||
|
||||
'timeout' and 'retries' kwargs must be passed to the function.
|
||||
|
||||
Args:
|
||||
f: The function to decorate.
|
||||
Returns:
|
||||
The decorated function.
|
||||
"""
|
||||
get_timeout = lambda *a, **kw: kw['timeout']
|
||||
get_retries = lambda *a, **kw: kw['retries']
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries)
|
||||
|
||||
|
||||
def WithExplicitTimeoutAndRetries(timeout, retries):
|
||||
"""Returns a decorator that handles timeouts and retries.
|
||||
|
||||
The provided |timeout| and |retries| values are always used.
|
||||
|
||||
Args:
|
||||
timeout: The number of seconds to wait for the decorated function to
|
||||
return. Always used.
|
||||
retries: The number of times the decorated function should be retried on
|
||||
failure. Always used.
|
||||
Returns:
|
||||
The actual decorator.
|
||||
"""
|
||||
def decorator(f):
|
||||
get_timeout = lambda *a, **kw: timeout
|
||||
get_retries = lambda *a, **kw: retries
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries)
|
||||
return decorator
|
||||
|
||||
|
||||
def WithTimeoutAndRetriesDefaults(default_timeout, default_retries):
|
||||
"""Returns a decorator that handles timeouts and retries.
|
||||
|
||||
The provided |default_timeout| and |default_retries| values are used only
|
||||
if timeout and retries values are not provided.
|
||||
|
||||
Args:
|
||||
default_timeout: The number of seconds to wait for the decorated function
|
||||
to return. Only used if a 'timeout' kwarg is not passed
|
||||
to the decorated function.
|
||||
default_retries: The number of times the decorated function should be
|
||||
retried on failure. Only used if a 'retries' kwarg is not
|
||||
passed to the decorated function.
|
||||
Returns:
|
||||
The actual decorator.
|
||||
"""
|
||||
def decorator(f):
|
||||
get_timeout = lambda *a, **kw: kw.get('timeout', default_timeout)
|
||||
get_retries = lambda *a, **kw: kw.get('retries', default_retries)
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries, pass_values=True)
|
||||
return decorator
|
||||
|
||||
|
||||
def WithTimeoutAndRetriesFromInstance(
|
||||
default_timeout_name=DEFAULT_TIMEOUT_ATTR,
|
||||
default_retries_name=DEFAULT_RETRIES_ATTR):
|
||||
"""Returns a decorator that handles timeouts and retries.
|
||||
|
||||
The provided |default_timeout_name| and |default_retries_name| are used to
|
||||
get the default timeout value and the default retries value from the object
|
||||
instance if timeout and retries values are not provided.
|
||||
|
||||
Note that this should only be used to decorate methods, not functions.
|
||||
|
||||
Args:
|
||||
default_timeout_name: The name of the default timeout attribute of the
|
||||
instance.
|
||||
default_retries_name: The name of the default retries attribute of the
|
||||
instance.
|
||||
Returns:
|
||||
The actual decorator.
|
||||
"""
|
||||
def decorator(f):
|
||||
def get_timeout(inst, *_args, **kwargs):
|
||||
return kwargs.get('timeout', getattr(inst, default_timeout_name))
|
||||
def get_retries(inst, *_args, **kwargs):
|
||||
return kwargs.get('retries', getattr(inst, default_retries_name))
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries, pass_values=True)
|
||||
return decorator
|
||||
|
|
@ -14,10 +14,10 @@ import time
|
|||
import traceback
|
||||
import unittest
|
||||
|
||||
from devil.android import decorators
|
||||
from devil.android import device_errors
|
||||
from devil.utils import reraiser_thread
|
||||
from pylib import constants
|
||||
from pylib.device import decorators
|
||||
from pylib.device import device_errors
|
||||
from pylib.utils import reraiser_thread
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 3
|
|
@ -0,0 +1,82 @@
|
|||
# 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 json
|
||||
import os
|
||||
import threading
|
||||
|
||||
from pylib import constants
|
||||
|
||||
# TODO(jbudorick): Remove this once the blacklist is optional.
|
||||
BLACKLIST_JSON = os.path.join(
|
||||
constants.DIR_SOURCE_ROOT,
|
||||
os.environ.get('CHROMIUM_OUT_DIR', 'out'),
|
||||
'bad_devices.json')
|
||||
|
||||
class Blacklist(object):
|
||||
|
||||
def __init__(self, path):
|
||||
self._blacklist_lock = threading.RLock()
|
||||
self._path = path
|
||||
|
||||
def Read(self):
|
||||
"""Reads the blacklist from the blacklist file.
|
||||
|
||||
Returns:
|
||||
A list containing bad devices.
|
||||
"""
|
||||
with self._blacklist_lock:
|
||||
if not os.path.exists(self._path):
|
||||
return []
|
||||
|
||||
with open(self._path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def Write(self, blacklist):
|
||||
"""Writes the provided blacklist to the blacklist file.
|
||||
|
||||
Args:
|
||||
blacklist: list of bad devices to write to the blacklist file.
|
||||
"""
|
||||
with self._blacklist_lock:
|
||||
with open(self._path, 'w') as f:
|
||||
json.dump(list(set(blacklist)), f)
|
||||
|
||||
def Extend(self, devices):
|
||||
"""Adds devices to blacklist file.
|
||||
|
||||
Args:
|
||||
devices: list of bad devices to be added to the blacklist file.
|
||||
"""
|
||||
with self._blacklist_lock:
|
||||
blacklist = ReadBlacklist()
|
||||
blacklist.extend(devices)
|
||||
WriteBlacklist(blacklist)
|
||||
|
||||
def Reset(self):
|
||||
"""Erases the blacklist file if it exists."""
|
||||
with self._blacklist_lock:
|
||||
if os.path.exists(self._path):
|
||||
os.remove(self._path)
|
||||
|
||||
|
||||
def ReadBlacklist():
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
return Blacklist(BLACKLIST_JSON).Read()
|
||||
|
||||
|
||||
def WriteBlacklist(blacklist):
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
Blacklist(BLACKLIST_JSON).Write(blacklist)
|
||||
|
||||
|
||||
def ExtendBlacklist(devices):
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
Blacklist(BLACKLIST_JSON).Extend(devices)
|
||||
|
||||
|
||||
def ResetBlacklist():
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
Blacklist(BLACKLIST_JSON).Reset()
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Exception classes raised by AdbWrapper and DeviceUtils.
|
||||
"""
|
||||
|
||||
from devil import base_error
|
||||
from devil.utils import cmd_helper
|
||||
|
||||
|
||||
class CommandFailedError(base_error.BaseError):
|
||||
"""Exception for command failures."""
|
||||
|
||||
def __init__(self, message, device_serial=None):
|
||||
if device_serial is not None:
|
||||
message = '(device: %s) %s' % (device_serial, message)
|
||||
self.device_serial = device_serial
|
||||
super(CommandFailedError, self).__init__(message)
|
||||
|
||||
|
||||
class AdbCommandFailedError(CommandFailedError):
|
||||
"""Exception for adb command failures."""
|
||||
|
||||
def __init__(self, args, output, status=None, device_serial=None,
|
||||
message=None):
|
||||
self.args = args
|
||||
self.output = output
|
||||
self.status = status
|
||||
if not message:
|
||||
adb_cmd = ' '.join(cmd_helper.SingleQuote(arg) for arg in self.args)
|
||||
message = ['adb %s: failed ' % adb_cmd]
|
||||
if status:
|
||||
message.append('with exit status %s ' % self.status)
|
||||
if output:
|
||||
message.append('and output:\n')
|
||||
message.extend('- %s\n' % line for line in output.splitlines())
|
||||
else:
|
||||
message.append('and no output.')
|
||||
message = ''.join(message)
|
||||
super(AdbCommandFailedError, self).__init__(message, device_serial)
|
||||
|
||||
|
||||
class DeviceVersionError(CommandFailedError):
|
||||
"""Exception for device version failures."""
|
||||
|
||||
def __init__(self, message, device_serial=None):
|
||||
super(DeviceVersionError, self).__init__(message, device_serial)
|
||||
|
||||
|
||||
class AdbShellCommandFailedError(AdbCommandFailedError):
|
||||
"""Exception for shell command failures run via adb."""
|
||||
|
||||
def __init__(self, command, output, status, device_serial=None):
|
||||
self.command = command
|
||||
message = ['shell command run via adb failed on the device:\n',
|
||||
' command: %s\n' % command]
|
||||
message.append(' exit status: %s\n' % status)
|
||||
if output:
|
||||
message.append(' output:\n')
|
||||
if isinstance(output, basestring):
|
||||
output_lines = output.splitlines()
|
||||
else:
|
||||
output_lines = output
|
||||
message.extend(' - %s\n' % line for line in output_lines)
|
||||
else:
|
||||
message.append(" output: ''\n")
|
||||
message = ''.join(message)
|
||||
super(AdbShellCommandFailedError, self).__init__(
|
||||
['shell', command], output, status, device_serial, message)
|
||||
|
||||
|
||||
class CommandTimeoutError(base_error.BaseError):
|
||||
"""Exception for command timeouts."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceUnreachableError(base_error.BaseError):
|
||||
"""Exception for device unreachable failures."""
|
||||
pass
|
||||
|
||||
|
||||
class NoDevicesError(base_error.BaseError):
|
||||
"""Exception for having no devices attached."""
|
||||
|
||||
def __init__(self):
|
||||
super(NoDevicesError, self).__init__(
|
||||
'No devices attached.', is_infra_error=True)
|
|
@ -0,0 +1,30 @@
|
|||
# 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.
|
||||
|
||||
"""A module to keep track of devices across builds."""
|
||||
|
||||
import os
|
||||
|
||||
LAST_DEVICES_FILENAME = '.last_devices'
|
||||
LAST_MISSING_DEVICES_FILENAME = '.last_missing'
|
||||
|
||||
|
||||
def GetPersistentDeviceList(file_name):
|
||||
"""Returns a list of devices.
|
||||
|
||||
Args:
|
||||
file_name: the file name containing a list of devices.
|
||||
|
||||
Returns: List of device serial numbers that were on the bot.
|
||||
"""
|
||||
with open(file_name) as f:
|
||||
return f.read().splitlines()
|
||||
|
||||
|
||||
def WritePersistentDeviceList(file_name, device_list):
|
||||
path = os.path.dirname(file_name)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
with open(file_name, 'w') as f:
|
||||
f.write('\n'.join(set(device_list)))
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Defines constants for signals that should be supported on devices.
|
||||
|
||||
Note: Obtained by running `kill -l` on a user device.
|
||||
"""
|
||||
|
||||
|
||||
SIGHUP = 1 # Hangup
|
||||
SIGINT = 2 # Interrupt
|
||||
SIGQUIT = 3 # Quit
|
||||
SIGILL = 4 # Illegal instruction
|
||||
SIGTRAP = 5 # Trap
|
||||
SIGABRT = 6 # Aborted
|
||||
SIGBUS = 7 # Bus error
|
||||
SIGFPE = 8 # Floating point exception
|
||||
SIGKILL = 9 # Killed
|
||||
SIGUSR1 = 10 # User signal 1
|
||||
SIGSEGV = 11 # Segmentation fault
|
||||
SIGUSR2 = 12 # User signal 2
|
||||
SIGPIPE = 13 # Broken pipe
|
||||
SIGALRM = 14 # Alarm clock
|
||||
SIGTERM = 15 # Terminated
|
||||
SIGSTKFLT = 16 # Stack fault
|
||||
SIGCHLD = 17 # Child exited
|
||||
SIGCONT = 18 # Continue
|
||||
SIGSTOP = 19 # Stopped (signal)
|
||||
SIGTSTP = 20 # Stopped
|
||||
SIGTTIN = 21 # Stopped (tty input)
|
||||
SIGTTOU = 22 # Stopped (tty output)
|
||||
SIGURG = 23 # Urgent I/O condition
|
||||
SIGXCPU = 24 # CPU time limit exceeded
|
||||
SIGXFSZ = 25 # File size limit exceeded
|
||||
SIGVTALRM = 26 # Virtual timer expired
|
||||
SIGPROF = 27 # Profiling timer expired
|
||||
SIGWINCH = 28 # Window size changed
|
||||
SIGIO = 29 # I/O possible
|
||||
SIGPWR = 30 # Power failure
|
||||
SIGSYS = 31 # Bad system call
|
|
@ -0,0 +1,63 @@
|
|||
# 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.
|
||||
|
||||
"""A temp file that automatically gets pushed and deleted from a device."""
|
||||
|
||||
# pylint: disable=W0622
|
||||
|
||||
import threading
|
||||
|
||||
from devil.android import device_errors
|
||||
from devil.utils import cmd_helper
|
||||
|
||||
_COMMAND_TEMPLATE = (
|
||||
# Make sure that the temp dir is writable
|
||||
'test -d {dir} && '
|
||||
# If 5 random attempts fail, something is up.
|
||||
'for i in 1 2 3 4 5; do '
|
||||
'fn={dir}/{prefix}-$(date +%s)-"$RANDOM"{suffix};'
|
||||
'test -e "$fn" || break;'
|
||||
'done && '
|
||||
# Touch the file, so other temp files can't get the same name.
|
||||
'touch "$fn" && echo -n "$fn"')
|
||||
|
||||
class DeviceTempFile(object):
|
||||
def __init__(self, adb, suffix='', prefix='temp_file', dir='/data/local/tmp'):
|
||||
"""Find an unused temporary file path in the devices external directory.
|
||||
|
||||
When this object is closed, the file will be deleted on the device.
|
||||
|
||||
Args:
|
||||
adb: An instance of AdbWrapper
|
||||
suffix: The suffix of the name of the temp file.
|
||||
prefix: The prefix of the name of the temp file.
|
||||
dir: The directory on the device where to place the temp file.
|
||||
"""
|
||||
self._adb = adb
|
||||
command = _COMMAND_TEMPLATE.format(
|
||||
dir=cmd_helper.SingleQuote(dir),
|
||||
suffix=cmd_helper.SingleQuote(suffix),
|
||||
prefix=cmd_helper.SingleQuote(prefix))
|
||||
self.name = self._adb.Shell(command)
|
||||
self.name_quoted = cmd_helper.SingleQuote(self.name)
|
||||
|
||||
def close(self):
|
||||
"""Deletes the temporary file from the device."""
|
||||
# ignore exception if the file is already gone.
|
||||
def helper():
|
||||
try:
|
||||
self._adb.Shell('rm -f %s' % self.name_quoted, expect_status=None)
|
||||
except device_errors.AdbCommandFailedError:
|
||||
# file does not exist on Android version without 'rm -f' support (ICS)
|
||||
pass
|
||||
|
||||
# It shouldn't matter when the temp file gets deleted, so do so
|
||||
# asynchronously.
|
||||
threading.Thread(target=helper).start()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.close()
|
|
@ -12,11 +12,11 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from devil.android import device_errors
|
||||
from devil.android.sdk import adb_wrapper
|
||||
from devil.utils import device_temp_file
|
||||
from devil.utils import mock_calls
|
||||
from pylib import constants
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import device_errors
|
||||
from pylib.utils import device_temp_file
|
||||
from pylib.utils import mock_calls
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -12,11 +12,11 @@ import os
|
|||
import tempfile
|
||||
import unittest
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import device_utils
|
||||
from pylib.utils import md5sum
|
||||
from devil.android import device_utils
|
||||
from devil.android.sdk import adb_wrapper
|
||||
from devil.utils import md5sum
|
||||
from devil.utils import cmd_helper
|
||||
|
||||
_OLD_CONTENTS = "foo"
|
||||
_NEW_CONTENTS = "bar"
|
|
@ -19,16 +19,16 @@ import re
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from pylib import cmd_helper
|
||||
from devil.android import device_signal
|
||||
from devil.android import device_blacklist
|
||||
from devil.android import device_errors
|
||||
from devil.android import device_utils
|
||||
from devil.android.sdk import adb_wrapper
|
||||
from devil.android.sdk import intent
|
||||
from devil.android.sdk import split_select
|
||||
from devil.utils import cmd_helper
|
||||
from devil.utils import mock_calls
|
||||
from pylib import constants
|
||||
from pylib import device_signal
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import device_blacklist
|
||||
from pylib.device import device_errors
|
||||
from pylib.device import device_utils
|
||||
from pylib.device import intent
|
||||
from pylib.sdk import split_select
|
||||
from pylib.utils import mock_calls
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
||||
|
@ -64,7 +64,8 @@ class DeviceUtilsGetAVDsTest(mock_calls.TestCase):
|
|||
|
||||
def testGetAVDs(self):
|
||||
with self.assertCall(
|
||||
mock.call.pylib.cmd_helper.GetCmdOutput([mock.ANY, 'list', 'avd']),
|
||||
mock.call.devil.utils.cmd_helper.GetCmdOutput(
|
||||
[mock.ANY, 'list', 'avd']),
|
||||
'Available Android Virtual Devices:\n'
|
||||
' Name: my_android5.0\n'
|
||||
' Path: /some/path/to/.android/avd/my_android5.0.avd\n'
|
||||
|
@ -80,13 +81,16 @@ class DeviceUtilsRestartServerTest(mock_calls.TestCase):
|
|||
@mock.patch('time.sleep', mock.Mock())
|
||||
def testRestartServer_succeeds(self):
|
||||
with self.assertCalls(
|
||||
mock.call.pylib.device.adb_wrapper.AdbWrapper.KillServer(),
|
||||
(mock.call.pylib.cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb']),
|
||||
mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.KillServer(),
|
||||
(mock.call.devil.utils.cmd_helper.GetCmdStatusAndOutput(
|
||||
['pgrep', 'adb']),
|
||||
(1, '')),
|
||||
mock.call.pylib.device.adb_wrapper.AdbWrapper.StartServer(),
|
||||
(mock.call.pylib.cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb']),
|
||||
mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.StartServer(),
|
||||
(mock.call.devil.utils.cmd_helper.GetCmdStatusAndOutput(
|
||||
['pgrep', 'adb']),
|
||||
(1, '')),
|
||||
(mock.call.pylib.cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb']),
|
||||
(mock.call.devil.utils.cmd_helper.GetCmdStatusAndOutput(
|
||||
['pgrep', 'adb']),
|
||||
(0, '123\n'))):
|
||||
device_utils.RestartServer()
|
||||
|
||||
|
@ -548,7 +552,8 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
|
|||
|
||||
def testInstall_noPriorInstall(self):
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName(
|
||||
'/fake/test/app.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'), []),
|
||||
self.call.adb.Install('/fake/test/app.apk', reinstall=False)):
|
||||
|
@ -556,7 +561,8 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
|
|||
|
||||
def testInstall_differentPriorInstall(self):
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName(
|
||||
'/fake/test/app.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'),
|
||||
['/fake/data/app/test.package.apk']),
|
||||
|
@ -569,7 +575,8 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
|
|||
|
||||
def testInstall_differentPriorInstall_reinstall(self):
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName(
|
||||
'/fake/test/app.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'),
|
||||
['/fake/data/app/test.package.apk']),
|
||||
|
@ -581,7 +588,8 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
|
|||
|
||||
def testInstall_identicalPriorInstall(self):
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName(
|
||||
'/fake/test/app.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'),
|
||||
['/fake/data/app/test.package.apk']),
|
||||
|
@ -592,7 +600,8 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
|
|||
|
||||
def testInstall_fails(self):
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('/fake/test/app.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName(
|
||||
'/fake/test/app.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'), []),
|
||||
(self.call.adb.Install('/fake/test/app.apk', reinstall=False),
|
||||
|
@ -605,11 +614,11 @@ class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
|
|||
def testInstallSplitApk_noPriorInstall(self):
|
||||
with self.assertCalls(
|
||||
(self.call.device._CheckSdkLevel(21)),
|
||||
(mock.call.pylib.sdk.split_select.SelectSplits(
|
||||
(mock.call.devil.android.sdk.split_select.SelectSplits(
|
||||
self.device, 'base.apk',
|
||||
['split1.apk', 'split2.apk', 'split3.apk']),
|
||||
['split2.apk']),
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('base.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName('base.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'), []),
|
||||
(self.call.adb.InstallMultiple(
|
||||
|
@ -620,11 +629,11 @@ class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
|
|||
def testInstallSplitApk_partialInstall(self):
|
||||
with self.assertCalls(
|
||||
(self.call.device._CheckSdkLevel(21)),
|
||||
(mock.call.pylib.sdk.split_select.SelectSplits(
|
||||
(mock.call.devil.android.sdk.split_select.SelectSplits(
|
||||
self.device, 'base.apk',
|
||||
['split1.apk', 'split2.apk', 'split3.apk']),
|
||||
['split2.apk']),
|
||||
(mock.call.pylib.utils.apk_helper.GetPackageName('base.apk'),
|
||||
(mock.call.devil.android.apk_helper.GetPackageName('base.apk'),
|
||||
'test.package'),
|
||||
(self.call.device._GetApplicationPathsInternal('test.package'),
|
||||
['base-on-device.apk', 'split2-on-device.apk']),
|
||||
|
@ -704,7 +713,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
|
|||
payload = 'hi! ' * 1024
|
||||
expected_cmd = "echo '%s'" % payload
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(
|
||||
self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
|
||||
self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
|
||||
(self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
|
||||
|
@ -718,7 +727,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
|
|||
with self.assertCalls(
|
||||
(self.call.device.NeedsSU(), True),
|
||||
(self.call.device._Su(expected_cmd_without_su), expected_cmd),
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(
|
||||
self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
|
||||
self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
|
||||
(self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
|
||||
|
@ -798,7 +807,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
|
|||
temp_file = MockTempFile('/sdcard/temp-123')
|
||||
cmd_redirect = '%s > %s' % (cmd, temp_file.name)
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(self.adb),
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
|
||||
temp_file),
|
||||
(self.call.adb.Shell(cmd_redirect)),
|
||||
(self.call.device.ReadFile(temp_file.name, force_pull=True),
|
||||
|
@ -820,7 +829,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
|
|||
cmd_redirect = '%s > %s' % (cmd, temp_file.name)
|
||||
with self.assertCalls(
|
||||
(self.call.adb.Shell(cmd), self.ShellError('', None)),
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(self.adb),
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
|
||||
temp_file),
|
||||
(self.call.adb.Shell(cmd_redirect)),
|
||||
(self.call.device.ReadFile(mock.ANY, force_pull=True),
|
||||
|
@ -1518,7 +1527,7 @@ class DeviceUtilsReadFileTest(DeviceUtilsTest):
|
|||
as_root=True, check_return=True),
|
||||
['-rw------- root root 123456 1970-01-01 00:00 can.be.read.with.su']),
|
||||
(self.call.device.NeedsSU(), True),
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(self.adb),
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
|
||||
MockTempFile('/sdcard/tmp/on.device')),
|
||||
self.call.device.RunShellCommand(
|
||||
['cp', '/this/big/file/can.be.read.with.su',
|
||||
|
@ -1578,7 +1587,7 @@ class DeviceUtilsWriteFileTest(DeviceUtilsTest):
|
|||
contents = 'some large contents ' * 26 # 20 * 26 = 520 chars
|
||||
with self.assertCalls(
|
||||
(self.call.device.NeedsSU(), True),
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(self.adb),
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
|
||||
MockTempFile('/sdcard/tmp/on.device')),
|
||||
self.call.device._WriteFileWithPush('/sdcard/tmp/on.device', contents),
|
||||
self.call.device.RunShellCommand(
|
||||
|
@ -1808,7 +1817,7 @@ class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest):
|
|||
|
||||
def testTakeScreenshot_fileNameProvided(self):
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(
|
||||
self.adb, suffix='.png'),
|
||||
MockTempFile('/tmp/path/temp-123.png')),
|
||||
(self.call.adb.Shell('/system/bin/screencap -p /tmp/path/temp-123.png'),
|
||||
|
@ -1958,7 +1967,7 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
|
|||
def testHealthyDevices_emptyBlacklist(self):
|
||||
test_serials = ['0123456789abcdef', 'fedcba9876543210']
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.device.adb_wrapper.AdbWrapper.Devices(),
|
||||
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
|
||||
[self._createAdbWrapperMock(s) for s in test_serials])):
|
||||
blacklist = mock.NonCallableMock(**{'Read.return_value': []})
|
||||
devices = device_utils.DeviceUtils.HealthyDevices(blacklist)
|
||||
|
@ -1969,7 +1978,7 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
|
|||
def testHealthyDevices_blacklist(self):
|
||||
test_serials = ['0123456789abcdef', 'fedcba9876543210']
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.device.adb_wrapper.AdbWrapper.Devices(),
|
||||
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
|
||||
[self._createAdbWrapperMock(s) for s in test_serials])):
|
||||
blacklist = mock.NonCallableMock(
|
||||
**{'Read.return_value': ['fedcba9876543210']})
|
||||
|
@ -1984,7 +1993,7 @@ class DeviceUtilsRestartAdbdTest(DeviceUtilsTest):
|
|||
def testAdbdRestart(self):
|
||||
mock_temp_file = '/sdcard/temp-123.sh'
|
||||
with self.assertCalls(
|
||||
(mock.call.pylib.utils.device_temp_file.DeviceTempFile(
|
||||
(mock.call.devil.android.device_temp_file.DeviceTempFile(
|
||||
self.adb, suffix='.sh'), MockTempFile(mock_temp_file)),
|
||||
self.call.device.WriteFile(mock.ANY, mock.ANY),
|
||||
(self.call.device.RunShellCommand(
|
|
@ -0,0 +1,139 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import re
|
||||
|
||||
from devil.android import decorators
|
||||
from devil.android import device_errors
|
||||
from devil.android.sdk import adb_wrapper
|
||||
|
||||
|
||||
class LogcatMonitor(object):
|
||||
|
||||
_THREADTIME_RE_FORMAT = (
|
||||
r'(?P<date>\S*) +(?P<time>\S*) +(?P<proc_id>%s) +(?P<thread_id>%s) +'
|
||||
r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')
|
||||
|
||||
def __init__(self, adb, clear=True, filter_specs=None):
|
||||
"""Create a LogcatMonitor instance.
|
||||
|
||||
Args:
|
||||
adb: An instance of adb_wrapper.AdbWrapper.
|
||||
clear: If True, clear the logcat when monitoring starts.
|
||||
filter_specs: An optional list of '<tag>[:priority]' strings.
|
||||
"""
|
||||
if isinstance(adb, adb_wrapper.AdbWrapper):
|
||||
self._adb = adb
|
||||
else:
|
||||
raise ValueError('Unsupported type passed for argument "device"')
|
||||
self._clear = clear
|
||||
self._filter_specs = filter_specs
|
||||
self._logcat_out = None
|
||||
self._logcat_out_file = None
|
||||
self._logcat_proc = None
|
||||
|
||||
@decorators.WithTimeoutAndRetriesDefaults(10, 0)
|
||||
def WaitFor(self, success_regex, failure_regex=None, timeout=None,
|
||||
retries=None):
|
||||
"""Wait for a matching logcat line or until a timeout occurs.
|
||||
|
||||
This will attempt to match lines in the logcat against both |success_regex|
|
||||
and |failure_regex| (if provided). Note that this calls re.search on each
|
||||
logcat line, not re.match, so the provided regular expressions don't have
|
||||
to match an entire line.
|
||||
|
||||
Args:
|
||||
success_regex: The regular expression to search for.
|
||||
failure_regex: An optional regular expression that, if hit, causes this
|
||||
to stop looking for a match. Can be None.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
A match object if |success_regex| matches a part of a logcat line, or
|
||||
None if |failure_regex| matches a part of a logcat line.
|
||||
Raises:
|
||||
CommandFailedError on logcat failure (NOT on a |failure_regex| match).
|
||||
CommandTimeoutError if no logcat line matching either |success_regex| or
|
||||
|failure_regex| is found in |timeout| seconds.
|
||||
DeviceUnreachableError if the device becomes unreachable.
|
||||
"""
|
||||
if isinstance(success_regex, basestring):
|
||||
success_regex = re.compile(success_regex)
|
||||
if isinstance(failure_regex, basestring):
|
||||
failure_regex = re.compile(failure_regex)
|
||||
|
||||
logging.debug('Waiting %d seconds for "%s"', timeout, success_regex.pattern)
|
||||
|
||||
# NOTE This will continue looping until:
|
||||
# - success_regex matches a line, in which case the match object is
|
||||
# returned.
|
||||
# - failure_regex matches a line, in which case None is returned
|
||||
# - the timeout is hit, in which case a CommandTimeoutError is raised.
|
||||
for l in self._adb.Logcat(filter_specs=self._filter_specs):
|
||||
m = success_regex.search(l)
|
||||
if m:
|
||||
return m
|
||||
if failure_regex and failure_regex.search(l):
|
||||
return None
|
||||
|
||||
def FindAll(self, message_regex, proc_id=None, thread_id=None, log_level=None,
|
||||
component=None):
|
||||
"""Finds all lines in the logcat that match the provided constraints.
|
||||
|
||||
Args:
|
||||
message_regex: The regular expression that the <message> section must
|
||||
match.
|
||||
proc_id: The process ID to match. If None, matches any process ID.
|
||||
thread_id: The thread ID to match. If None, matches any thread ID.
|
||||
log_level: The log level to match. If None, matches any log level.
|
||||
component: The component to match. If None, matches any component.
|
||||
|
||||
Yields:
|
||||
A match object for each matching line in the logcat. The match object
|
||||
will always contain, in addition to groups defined in |message_regex|,
|
||||
the following named groups: 'date', 'time', 'proc_id', 'thread_id',
|
||||
'log_level', 'component', and 'message'.
|
||||
"""
|
||||
if proc_id is None:
|
||||
proc_id = r'\d+'
|
||||
if thread_id is None:
|
||||
thread_id = r'\d+'
|
||||
if log_level is None:
|
||||
log_level = r'[VDIWEF]'
|
||||
if component is None:
|
||||
component = r'[^\s:]+'
|
||||
threadtime_re = re.compile(
|
||||
type(self)._THREADTIME_RE_FORMAT % (
|
||||
proc_id, thread_id, log_level, component, message_regex))
|
||||
|
||||
for line in self._adb.Logcat(dump=True, logcat_format='threadtime'):
|
||||
m = re.match(threadtime_re, line)
|
||||
if m:
|
||||
yield m
|
||||
|
||||
def Start(self):
|
||||
"""Starts the logcat monitor.
|
||||
|
||||
Clears the logcat if |clear| was set in |__init__|.
|
||||
"""
|
||||
if self._clear:
|
||||
self._adb.Logcat(clear=True)
|
||||
|
||||
def __enter__(self):
|
||||
"""Starts the logcat monitor."""
|
||||
self.Start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Stops the logcat monitor."""
|
||||
pass
|
|
@ -8,10 +8,10 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from devil.android import decorators
|
||||
from devil.android import logcat_monitor
|
||||
from devil.android.sdk import adb_wrapper
|
||||
from pylib import constants
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import decorators
|
||||
from pylib.device import logcat_monitor
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
|
@ -0,0 +1,115 @@
|
|||
# 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 collections
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
from devil.android import device_errors
|
||||
from devil.android import device_temp_file
|
||||
from devil.utils import cmd_helper
|
||||
from pylib import constants
|
||||
|
||||
MD5SUM_DEVICE_LIB_PATH = '/data/local/tmp/md5sum/'
|
||||
MD5SUM_DEVICE_BIN_PATH = MD5SUM_DEVICE_LIB_PATH + 'md5sum_bin'
|
||||
|
||||
_STARTS_WITH_CHECKSUM_RE = re.compile(r'^\s*[0-9a-fA-F]{32}\s+')
|
||||
|
||||
|
||||
def CalculateHostMd5Sums(paths):
|
||||
"""Calculates the MD5 sum value for all items in |paths|.
|
||||
|
||||
Directories are traversed recursively and the MD5 sum of each file found is
|
||||
reported in the result.
|
||||
|
||||
Args:
|
||||
paths: A list of host paths to md5sum.
|
||||
Returns:
|
||||
A dict mapping file paths to their respective md5sum checksums.
|
||||
"""
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
|
||||
md5sum_bin_host_path = os.path.join(
|
||||
constants.GetOutDirectory(), 'md5sum_bin_host')
|
||||
if not os.path.exists(md5sum_bin_host_path):
|
||||
raise IOError('File not built: %s' % md5sum_bin_host_path)
|
||||
out = cmd_helper.GetCmdOutput([md5sum_bin_host_path] + [p for p in paths])
|
||||
|
||||
return _ParseMd5SumOutput(out.splitlines())
|
||||
|
||||
|
||||
def CalculateDeviceMd5Sums(paths, device):
|
||||
"""Calculates the MD5 sum value for all items in |paths|.
|
||||
|
||||
Directories are traversed recursively and the MD5 sum of each file found is
|
||||
reported in the result.
|
||||
|
||||
Args:
|
||||
paths: A list of device paths to md5sum.
|
||||
Returns:
|
||||
A dict mapping file paths to their respective md5sum checksums.
|
||||
"""
|
||||
if not paths:
|
||||
return {}
|
||||
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
# Allow generators
|
||||
paths = list(paths)
|
||||
|
||||
md5sum_dist_path = os.path.join(constants.GetOutDirectory(), 'md5sum_dist')
|
||||
md5sum_dist_bin_path = os.path.join(md5sum_dist_path, 'md5sum_bin')
|
||||
|
||||
if not os.path.exists(md5sum_dist_path):
|
||||
raise IOError('File not built: %s' % md5sum_dist_path)
|
||||
md5sum_file_size = os.path.getsize(md5sum_dist_bin_path)
|
||||
|
||||
# For better performance, make the script as small as possible to try and
|
||||
# avoid needing to write to an intermediary file (which RunShellCommand will
|
||||
# do if necessary).
|
||||
md5sum_script = 'a=%s;' % MD5SUM_DEVICE_BIN_PATH
|
||||
# Check if the binary is missing or has changed (using its file size as an
|
||||
# indicator), and trigger a (re-)push via the exit code.
|
||||
md5sum_script += '! [[ $(ls -l $a) = *%d* ]]&&exit 2;' % md5sum_file_size
|
||||
# Make sure it can find libbase.so
|
||||
md5sum_script += 'export LD_LIBRARY_PATH=%s;' % MD5SUM_DEVICE_LIB_PATH
|
||||
if len(paths) > 1:
|
||||
prefix = posixpath.commonprefix(paths)
|
||||
if len(prefix) > 4:
|
||||
md5sum_script += 'p="%s";' % prefix
|
||||
paths = ['$p"%s"' % p[len(prefix):] for p in paths]
|
||||
|
||||
md5sum_script += ';'.join('$a %s' % p for p in paths)
|
||||
# Don't fail the script if the last md5sum fails (due to file not found)
|
||||
# Note: ":" is equivalent to "true".
|
||||
md5sum_script += ';:'
|
||||
try:
|
||||
out = device.RunShellCommand(md5sum_script, check_return=True)
|
||||
except device_errors.AdbShellCommandFailedError as e:
|
||||
# Push the binary only if it is found to not exist
|
||||
# (faster than checking up-front).
|
||||
if e.status == 2:
|
||||
# If files were previously pushed as root (adbd running as root), trying
|
||||
# to re-push as non-root causes the push command to report success, but
|
||||
# actually fail. So, wipe the directory first.
|
||||
device.RunShellCommand(['rm', '-rf', MD5SUM_DEVICE_LIB_PATH],
|
||||
as_root=True, check_return=True)
|
||||
device.adb.Push(md5sum_dist_path, MD5SUM_DEVICE_LIB_PATH)
|
||||
out = device.RunShellCommand(md5sum_script, check_return=True)
|
||||
else:
|
||||
raise
|
||||
|
||||
return _ParseMd5SumOutput(out)
|
||||
|
||||
|
||||
def _ParseMd5SumOutput(out):
|
||||
hash_and_path = (l.split(None, 1) for l in out
|
||||
if l and _STARTS_WITH_CHECKSUM_RE.match(l))
|
||||
return dict((p, h) for h, p in hash_and_path)
|
||||
|
|
@ -7,10 +7,10 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from pylib import cmd_helper
|
||||
from devil.android import device_errors
|
||||
from devil.android import md5sum
|
||||
from devil.utils import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.device import device_errors
|
||||
from pylib.utils import md5sum
|
||||
|
||||
sys.path.append(
|
||||
os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
||||
|
@ -39,7 +39,8 @@ class Md5SumTest(unittest.TestCase):
|
|||
test_path = '/test/host/file.dat'
|
||||
mock_get_cmd_output = mock.Mock(
|
||||
return_value='0123456789abcdeffedcba9876543210 /test/host/file.dat')
|
||||
with mock.patch('pylib.cmd_helper.GetCmdOutput', new=mock_get_cmd_output):
|
||||
with mock.patch('devil.utils.cmd_helper.GetCmdOutput',
|
||||
new=mock_get_cmd_output):
|
||||
out = md5sum.CalculateHostMd5Sums(test_path)
|
||||
self.assertEquals(1, len(out))
|
||||
self.assertTrue('/test/host/file.dat' in out)
|
||||
|
@ -53,7 +54,8 @@ class Md5SumTest(unittest.TestCase):
|
|||
mock_get_cmd_output = mock.Mock(
|
||||
return_value='0123456789abcdeffedcba9876543210 /test/host/file0.dat\n'
|
||||
'123456789abcdef00fedcba987654321 /test/host/file1.dat\n')
|
||||
with mock.patch('pylib.cmd_helper.GetCmdOutput', new=mock_get_cmd_output):
|
||||
with mock.patch('devil.utils.cmd_helper.GetCmdOutput',
|
||||
new=mock_get_cmd_output):
|
||||
out = md5sum.CalculateHostMd5Sums(test_paths)
|
||||
self.assertEquals(2, len(out))
|
||||
self.assertTrue('/test/host/file0.dat' in out)
|
||||
|
@ -71,7 +73,8 @@ class Md5SumTest(unittest.TestCase):
|
|||
mock_get_cmd_output = mock.Mock(
|
||||
return_value='0123456789abcdeffedcba9876543210 /test/host/file0.dat\n'
|
||||
'123456789abcdef00fedcba987654321 /test/host/file1.dat\n')
|
||||
with mock.patch('pylib.cmd_helper.GetCmdOutput', new=mock_get_cmd_output):
|
||||
with mock.patch('devil.utils.cmd_helper.GetCmdOutput',
|
||||
new=mock_get_cmd_output):
|
||||
out = md5sum.CalculateHostMd5Sums(test_paths)
|
||||
self.assertEquals(2, len(out))
|
||||
self.assertTrue('/test/host/file0.dat' in out)
|
|
@ -0,0 +1,6 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
# This package is intended for modules that are very tightly coupled to
|
||||
# tools or APIs from the Android SDK.
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""This module wraps the Android Asset Packaging Tool."""
|
||||
|
||||
import os
|
||||
|
||||
from devil.utils import cmd_helper
|
||||
from devil.utils import timeout_retry
|
||||
from pylib import constants
|
||||
|
||||
_AAPT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'aapt')
|
||||
|
||||
def _RunAaptCmd(args):
|
||||
"""Runs an aapt command.
|
||||
|
||||
Args:
|
||||
args: A list of arguments for aapt.
|
||||
|
||||
Returns:
|
||||
The output of the command.
|
||||
"""
|
||||
cmd = [_AAPT_PATH] + args
|
||||
status, output = cmd_helper.GetCmdStatusAndOutput(cmd)
|
||||
if status != 0:
|
||||
raise Exception('Failed running aapt command: "%s" with output "%s".' %
|
||||
(' '.join(cmd), output))
|
||||
return output
|
||||
|
||||
def Dump(what, apk, assets=None):
|
||||
"""Returns the output of the aapt dump command.
|
||||
|
||||
Args:
|
||||
what: What you want to dump.
|
||||
apk: Path to apk you want to dump information for.
|
||||
assets: List of assets in apk you want to dump information for.
|
||||
"""
|
||||
assets = assets or []
|
||||
if isinstance(assets, basestring):
|
||||
assets = [assets]
|
||||
return _RunAaptCmd(['dump', what, apk] + assets).splitlines()
|
|
@ -0,0 +1,649 @@
|
|||
# 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.
|
||||
|
||||
"""This module wraps Android's adb tool.
|
||||
|
||||
This is a thin wrapper around the adb interface. Any additional complexity
|
||||
should be delegated to a higher level (ex. DeviceUtils).
|
||||
"""
|
||||
|
||||
import collections
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from devil.android import decorators
|
||||
from devil.android import device_errors
|
||||
from devil.utils import cmd_helper
|
||||
from devil.utils import timeout_retry
|
||||
from pylib import constants
|
||||
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 2
|
||||
|
||||
_EMULATOR_RE = re.compile(r'^emulator-[0-9]+$')
|
||||
|
||||
_READY_STATE = 'device'
|
||||
|
||||
|
||||
def _VerifyLocalFileExists(path):
|
||||
"""Verifies a local file exists.
|
||||
|
||||
Args:
|
||||
path: Path to the local file.
|
||||
|
||||
Raises:
|
||||
IOError: If the file doesn't exist.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
|
||||
DeviceStat = collections.namedtuple('DeviceStat',
|
||||
['st_mode', 'st_size', 'st_time'])
|
||||
|
||||
|
||||
class AdbWrapper(object):
|
||||
"""A wrapper around a local Android Debug Bridge executable."""
|
||||
|
||||
def __init__(self, device_serial):
|
||||
"""Initializes the AdbWrapper.
|
||||
|
||||
Args:
|
||||
device_serial: The device serial number as a string.
|
||||
"""
|
||||
if not device_serial:
|
||||
raise ValueError('A device serial must be specified')
|
||||
self._device_serial = str(device_serial)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@classmethod
|
||||
def _BuildAdbCmd(cls, args, device_serial, cpu_affinity=None):
|
||||
if cpu_affinity is not None:
|
||||
cmd = ['taskset', '-c', str(cpu_affinity)]
|
||||
else:
|
||||
cmd = []
|
||||
cmd.append(constants.GetAdbPath())
|
||||
if device_serial is not None:
|
||||
cmd.extend(['-s', device_serial])
|
||||
cmd.extend(args)
|
||||
return cmd
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@classmethod
|
||||
@decorators.WithTimeoutAndRetries
|
||||
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None,
|
||||
check_error=True, cpu_affinity=None):
|
||||
status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
|
||||
cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity),
|
||||
timeout_retry.CurrentTimeoutThread().GetRemainingTime())
|
||||
if status != 0:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
args, output, status, device_serial)
|
||||
# This catches some errors, including when the device drops offline;
|
||||
# unfortunately adb is very inconsistent with error reporting so many
|
||||
# command failures present differently.
|
||||
if check_error and output.startswith('error:'):
|
||||
raise device_errors.AdbCommandFailedError(args, output)
|
||||
return output
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
def _RunDeviceAdbCmd(self, args, timeout, retries, check_error=True):
|
||||
"""Runs an adb command on the device associated with this object.
|
||||
|
||||
Args:
|
||||
args: A list of arguments to adb.
|
||||
timeout: Timeout in seconds.
|
||||
retries: Number of retries.
|
||||
check_error: Check that the command doesn't return an error message. This
|
||||
does NOT check the exit status of shell commands.
|
||||
|
||||
Returns:
|
||||
The output of the command.
|
||||
"""
|
||||
return self._RunAdbCmd(args, timeout=timeout, retries=retries,
|
||||
device_serial=self._device_serial,
|
||||
check_error=check_error)
|
||||
|
||||
def _IterRunDeviceAdbCmd(self, args, timeout):
|
||||
"""Runs an adb command and returns an iterator over its output lines.
|
||||
|
||||
Args:
|
||||
args: A list of arguments to adb.
|
||||
timeout: Timeout in seconds.
|
||||
|
||||
Yields:
|
||||
The output of the command line by line.
|
||||
"""
|
||||
return cmd_helper.IterCmdOutputLines(
|
||||
self._BuildAdbCmd(args, self._device_serial), timeout=timeout)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Consider instances equal if they refer to the same device.
|
||||
|
||||
Args:
|
||||
other: The instance to compare equality with.
|
||||
|
||||
Returns:
|
||||
True if the instances are considered equal, false otherwise.
|
||||
"""
|
||||
return self._device_serial == str(other)
|
||||
|
||||
def __str__(self):
|
||||
"""The string representation of an instance.
|
||||
|
||||
Returns:
|
||||
The device serial number as a string.
|
||||
"""
|
||||
return self._device_serial
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(\'%s\')' % (self.__class__.__name__, self)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@classmethod
|
||||
def IsServerOnline(cls):
|
||||
status, output = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb'])
|
||||
output = [int(x) for x in output.split()]
|
||||
logging.info('PIDs for adb found: %r', output)
|
||||
return status == 0
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
@classmethod
|
||||
def KillServer(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
cls._RunAdbCmd(['kill-server'], timeout=timeout, retries=retries)
|
||||
|
||||
@classmethod
|
||||
def StartServer(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
# CPU affinity is used to reduce adb instability http://crbug.com/268450
|
||||
cls._RunAdbCmd(['start-server'], timeout=timeout, retries=retries,
|
||||
cpu_affinity=0)
|
||||
|
||||
@classmethod
|
||||
def GetDevices(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""DEPRECATED. Refer to Devices(...) below."""
|
||||
# TODO(jbudorick): Remove this function once no more clients are using it.
|
||||
return cls.Devices(timeout=timeout, retries=retries)
|
||||
|
||||
@classmethod
|
||||
def Devices(cls, desired_state=_READY_STATE, long_list=False,
|
||||
timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Get the list of active attached devices.
|
||||
|
||||
Args:
|
||||
desired_state: If not None, limit the devices returned to only those
|
||||
in the given state.
|
||||
long_list: Whether to use the long listing format.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Yields:
|
||||
AdbWrapper instances.
|
||||
"""
|
||||
lines = cls._RawDevices(long_list=long_list, timeout=timeout,
|
||||
retries=retries)
|
||||
return [AdbWrapper(line[0]) for line in lines
|
||||
if ((long_list or len(line) == 2)
|
||||
and (not desired_state or line[1] == desired_state))]
|
||||
|
||||
@classmethod
|
||||
def _RawDevices(cls, long_list=False, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
cmd = ['devices']
|
||||
if long_list:
|
||||
cmd.append('-l')
|
||||
output = cls._RunAdbCmd(cmd, timeout=timeout, retries=retries)
|
||||
return [line.split() for line in output.splitlines()[1:]]
|
||||
|
||||
def GetDeviceSerial(self):
|
||||
"""Gets the device serial number associated with this object.
|
||||
|
||||
Returns:
|
||||
Device serial number as a string.
|
||||
"""
|
||||
return self._device_serial
|
||||
|
||||
def Push(self, local, remote, timeout=60*5, retries=_DEFAULT_RETRIES):
|
||||
"""Pushes a file from the host to the device.
|
||||
|
||||
Args:
|
||||
local: Path on the host filesystem.
|
||||
remote: Path on the device filesystem.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
_VerifyLocalFileExists(local)
|
||||
self._RunDeviceAdbCmd(['push', local, remote], timeout, retries)
|
||||
|
||||
def Pull(self, remote, local, timeout=60*5, retries=_DEFAULT_RETRIES):
|
||||
"""Pulls a file from the device to the host.
|
||||
|
||||
Args:
|
||||
remote: Path on the device filesystem.
|
||||
local: Path on the host filesystem.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
cmd = ['pull', remote, local]
|
||||
self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
try:
|
||||
_VerifyLocalFileExists(local)
|
||||
except IOError:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, 'File not found on host: %s' % local, device_serial=str(self))
|
||||
|
||||
def Shell(self, command, expect_status=0, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Runs a shell command on the device.
|
||||
|
||||
Args:
|
||||
command: A string with the shell command to run.
|
||||
expect_status: (optional) Check that the command's exit status matches
|
||||
this value. Default is 0. If set to None the test is skipped.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
The output of the shell command as a string.
|
||||
|
||||
Raises:
|
||||
device_errors.AdbCommandFailedError: If the exit status doesn't match
|
||||
|expect_status|.
|
||||
"""
|
||||
if expect_status is None:
|
||||
args = ['shell', command]
|
||||
else:
|
||||
args = ['shell', '(%s);echo %%$?' % command.rstrip()]
|
||||
output = self._RunDeviceAdbCmd(args, timeout, retries, check_error=False)
|
||||
if expect_status is not None:
|
||||
output_end = output.rfind('%')
|
||||
if output_end < 0:
|
||||
# causes the status string to become empty and raise a ValueError
|
||||
output_end = len(output)
|
||||
|
||||
try:
|
||||
status = int(output[output_end+1:])
|
||||
except ValueError:
|
||||
logging.warning('exit status of shell command %r missing.', command)
|
||||
raise device_errors.AdbShellCommandFailedError(
|
||||
command, output, status=None, device_serial=self._device_serial)
|
||||
output = output[:output_end]
|
||||
if status != expect_status:
|
||||
raise device_errors.AdbShellCommandFailedError(
|
||||
command, output, status=status, device_serial=self._device_serial)
|
||||
return output
|
||||
|
||||
def IterShell(self, command, timeout):
|
||||
"""Runs a shell command and returns an iterator over its output lines.
|
||||
|
||||
Args:
|
||||
command: A string with the shell command to run.
|
||||
timeout: Timeout in seconds.
|
||||
|
||||
Yields:
|
||||
The output of the command line by line.
|
||||
"""
|
||||
args = ['shell', command]
|
||||
return cmd_helper.IterCmdOutputLines(
|
||||
self._BuildAdbCmd(args, self._device_serial), timeout=timeout)
|
||||
|
||||
def Ls(self, path, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""List the contents of a directory on the device.
|
||||
|
||||
Args:
|
||||
path: Path on the device filesystem.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
A list of pairs (filename, stat) for each file found in the directory,
|
||||
where the stat object has the properties: st_mode, st_size, and st_time.
|
||||
|
||||
Raises:
|
||||
AdbCommandFailedError if |path| does not specify a valid and accessible
|
||||
directory in the device.
|
||||
"""
|
||||
def ParseLine(line):
|
||||
cols = line.split(None, 3)
|
||||
filename = cols.pop()
|
||||
stat = DeviceStat(*[int(num, base=16) for num in cols])
|
||||
return (filename, stat)
|
||||
|
||||
cmd = ['ls', path]
|
||||
lines = self._RunDeviceAdbCmd(
|
||||
cmd, timeout=timeout, retries=retries).splitlines()
|
||||
if lines:
|
||||
return [ParseLine(line) for line in lines]
|
||||
else:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, 'path does not specify an accessible directory in the device',
|
||||
device_serial=self._device_serial)
|
||||
|
||||
def Logcat(self, clear=False, dump=False, filter_specs=None,
|
||||
logcat_format=None, ring_buffer=None, timeout=None,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Get an iterable over the logcat output.
|
||||
|
||||
Args:
|
||||
clear: If true, clear the logcat.
|
||||
dump: If true, dump the current logcat contents.
|
||||
filter_specs: If set, a list of specs to filter the logcat.
|
||||
logcat_format: If set, the format in which the logcat should be output.
|
||||
Options include "brief", "process", "tag", "thread", "raw", "time",
|
||||
"threadtime", and "long"
|
||||
ring_buffer: If set, a list of alternate ring buffers to request.
|
||||
Options include "main", "system", "radio", "events", "crash" or "all".
|
||||
The default is equivalent to ["main", "system", "crash"].
|
||||
timeout: (optional) If set, timeout per try in seconds. If clear or dump
|
||||
is set, defaults to _DEFAULT_TIMEOUT.
|
||||
retries: (optional) If clear or dump is set, the number of retries to
|
||||
attempt. Otherwise, does nothing.
|
||||
|
||||
Yields:
|
||||
logcat output line by line.
|
||||
"""
|
||||
cmd = ['logcat']
|
||||
use_iter = True
|
||||
if clear:
|
||||
cmd.append('-c')
|
||||
use_iter = False
|
||||
if dump:
|
||||
cmd.append('-d')
|
||||
use_iter = False
|
||||
if logcat_format:
|
||||
cmd.extend(['-v', logcat_format])
|
||||
if ring_buffer:
|
||||
for buffer_name in ring_buffer:
|
||||
cmd.extend(['-b', buffer_name])
|
||||
if filter_specs:
|
||||
cmd.extend(filter_specs)
|
||||
|
||||
if use_iter:
|
||||
return self._IterRunDeviceAdbCmd(cmd, timeout)
|
||||
else:
|
||||
timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT
|
||||
return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines()
|
||||
|
||||
def Forward(self, local, remote, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Forward socket connections from the local socket to the remote socket.
|
||||
|
||||
Sockets are specified by one of:
|
||||
tcp:<port>
|
||||
localabstract:<unix domain socket name>
|
||||
localreserved:<unix domain socket name>
|
||||
localfilesystem:<unix domain socket name>
|
||||
dev:<character device name>
|
||||
jdwp:<process pid> (remote only)
|
||||
|
||||
Args:
|
||||
local: The host socket.
|
||||
remote: The device socket.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
self._RunDeviceAdbCmd(['forward', str(local), str(remote)], timeout,
|
||||
retries)
|
||||
|
||||
def ForwardRemove(self, local, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Remove a forward socket connection.
|
||||
|
||||
Args:
|
||||
local: The host socket.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
self._RunDeviceAdbCmd(['forward', '--remove', str(local)], timeout,
|
||||
retries)
|
||||
|
||||
def ForwardList(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""List all currently forwarded socket connections.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
return self._RunDeviceAdbCmd(['forward', '--list'], timeout, retries)
|
||||
|
||||
def JDWP(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""List of PIDs of processes hosting a JDWP transport.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
A list of PIDs as strings.
|
||||
"""
|
||||
return [a.strip() for a in
|
||||
self._RunDeviceAdbCmd(['jdwp'], timeout, retries).split('\n')]
|
||||
|
||||
def Install(self, apk_path, forward_lock=False, reinstall=False,
|
||||
sd_card=False, timeout=60*2, retries=_DEFAULT_RETRIES):
|
||||
"""Install an apk on the device.
|
||||
|
||||
Args:
|
||||
apk_path: Host path to the APK file.
|
||||
forward_lock: (optional) If set forward-locks the app.
|
||||
reinstall: (optional) If set reinstalls the app, keeping its data.
|
||||
sd_card: (optional) If set installs on the SD card.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
_VerifyLocalFileExists(apk_path)
|
||||
cmd = ['install']
|
||||
if forward_lock:
|
||||
cmd.append('-l')
|
||||
if reinstall:
|
||||
cmd.append('-r')
|
||||
if sd_card:
|
||||
cmd.append('-s')
|
||||
cmd.append(apk_path)
|
||||
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
if 'Success' not in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, output, device_serial=self._device_serial)
|
||||
|
||||
def InstallMultiple(self, apk_paths, forward_lock=False, reinstall=False,
|
||||
sd_card=False, allow_downgrade=False, partial=False,
|
||||
timeout=60*2, retries=_DEFAULT_RETRIES):
|
||||
"""Install an apk with splits on the device.
|
||||
|
||||
Args:
|
||||
apk_paths: Host path to the APK file.
|
||||
forward_lock: (optional) If set forward-locks the app.
|
||||
reinstall: (optional) If set reinstalls the app, keeping its data.
|
||||
sd_card: (optional) If set installs on the SD card.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
allow_downgrade: (optional) Allow versionCode downgrade.
|
||||
partial: (optional) Package ID if apk_paths doesn't include all .apks.
|
||||
"""
|
||||
for path in apk_paths:
|
||||
_VerifyLocalFileExists(path)
|
||||
cmd = ['install-multiple']
|
||||
if forward_lock:
|
||||
cmd.append('-l')
|
||||
if reinstall:
|
||||
cmd.append('-r')
|
||||
if sd_card:
|
||||
cmd.append('-s')
|
||||
if allow_downgrade:
|
||||
cmd.append('-d')
|
||||
if partial:
|
||||
cmd.extend(('-p', partial))
|
||||
cmd.extend(apk_paths)
|
||||
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
if 'Success' not in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, output, device_serial=self._device_serial)
|
||||
|
||||
def Uninstall(self, package, keep_data=False, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Remove the app |package| from the device.
|
||||
|
||||
Args:
|
||||
package: The package to uninstall.
|
||||
keep_data: (optional) If set keep the data and cache directories.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
cmd = ['uninstall']
|
||||
if keep_data:
|
||||
cmd.append('-k')
|
||||
cmd.append(package)
|
||||
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
if 'Failure' in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, output, device_serial=self._device_serial)
|
||||
|
||||
def Backup(self, path, packages=None, apk=False, shared=False,
|
||||
nosystem=True, include_all=False, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Write an archive of the device's data to |path|.
|
||||
|
||||
Args:
|
||||
path: Local path to store the backup file.
|
||||
packages: List of to packages to be backed up.
|
||||
apk: (optional) If set include the .apk files in the archive.
|
||||
shared: (optional) If set buckup the device's SD card.
|
||||
nosystem: (optional) If set exclude system applications.
|
||||
include_all: (optional) If set back up all installed applications and
|
||||
|packages| is optional.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
cmd = ['backup', '-f', path]
|
||||
if apk:
|
||||
cmd.append('-apk')
|
||||
if shared:
|
||||
cmd.append('-shared')
|
||||
if nosystem:
|
||||
cmd.append('-nosystem')
|
||||
if include_all:
|
||||
cmd.append('-all')
|
||||
if packages:
|
||||
cmd.extend(packages)
|
||||
assert bool(packages) ^ bool(include_all), (
|
||||
'Provide \'packages\' or set \'include_all\' but not both.')
|
||||
ret = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
_VerifyLocalFileExists(path)
|
||||
return ret
|
||||
|
||||
def Restore(self, path, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Restore device contents from the backup archive.
|
||||
|
||||
Args:
|
||||
path: Host path to the backup archive.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
_VerifyLocalFileExists(path)
|
||||
self._RunDeviceAdbCmd(['restore'] + [path], timeout, retries)
|
||||
|
||||
def WaitForDevice(self, timeout=60*5, retries=_DEFAULT_RETRIES):
|
||||
"""Block until the device is online.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
self._RunDeviceAdbCmd(['wait-for-device'], timeout, retries)
|
||||
|
||||
def GetState(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Get device state.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
One of 'offline', 'bootloader', or 'device'.
|
||||
"""
|
||||
# TODO(jbudorick): Revert to using get-state once it doesn't cause a
|
||||
# a protocol fault.
|
||||
# return self._RunDeviceAdbCmd(['get-state'], timeout, retries).strip()
|
||||
|
||||
lines = self._RawDevices(timeout=timeout, retries=retries)
|
||||
for line in lines:
|
||||
if len(line) >= 2 and line[0] == self._device_serial:
|
||||
return line[1]
|
||||
return 'offline'
|
||||
|
||||
|
||||
def GetDevPath(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Gets the device path.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
The device path (e.g. usb:3-4)
|
||||
"""
|
||||
return self._RunDeviceAdbCmd(['get-devpath'], timeout, retries)
|
||||
|
||||
def Remount(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Remounts the /system partition on the device read-write."""
|
||||
self._RunDeviceAdbCmd(['remount'], timeout, retries)
|
||||
|
||||
def Reboot(self, to_bootloader=False, timeout=60*5,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Reboots the device.
|
||||
|
||||
Args:
|
||||
to_bootloader: (optional) If set reboots to the bootloader.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
if to_bootloader:
|
||||
cmd = ['reboot-bootloader']
|
||||
else:
|
||||
cmd = ['reboot']
|
||||
self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
|
||||
def Root(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Restarts the adbd daemon with root permissions, if possible.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
output = self._RunDeviceAdbCmd(['root'], timeout, retries)
|
||||
if 'cannot' in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
['root'], output, device_serial=self._device_serial)
|
||||
|
||||
def Emu(self, cmd, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Runs an emulator console command.
|
||||
|
||||
See http://developer.android.com/tools/devices/emulator.html#console
|
||||
|
||||
Args:
|
||||
cmd: The command to run on the emulator console.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
The output of the emulator console command.
|
||||
"""
|
||||
if isinstance(cmd, basestring):
|
||||
cmd = [cmd]
|
||||
return self._RunDeviceAdbCmd(['emu'] + cmd, timeout, retries)
|
||||
|
||||
@property
|
||||
def is_emulator(self):
|
||||
return _EMULATOR_RE.match(self._device_serial)
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
try:
|
||||
return self.GetState() == _READY_STATE
|
||||
except device_errors.CommandFailedError:
|
||||
return False
|
|
@ -9,8 +9,8 @@ import tempfile
|
|||
import time
|
||||
import unittest
|
||||
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import device_errors
|
||||
from devil.android import device_errors
|
||||
from devil.android.sdk import adb_wrapper
|
||||
|
||||
|
||||
class TestAdbWrapper(unittest.TestCase):
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2015 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
|
||||
|
||||
from devil.utils import cmd_helper
|
||||
from pylib import constants
|
||||
|
||||
_DEXDUMP_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'dexdump')
|
||||
|
||||
def DexDump(dexfiles, file_summary=False):
|
||||
"""A wrapper around the Android SDK's dexdump tool.
|
||||
|
||||
Args:
|
||||
dexfiles: The dexfile or list of dex files to dump.
|
||||
file_summary: Display summary information from the file header. (-f)
|
||||
|
||||
Returns:
|
||||
An iterable over the output lines.
|
||||
"""
|
||||
# TODO(jbudorick): Add support for more options as necessary.
|
||||
if isinstance(dexfiles, basestring):
|
||||
dexfiles = [dexfiles]
|
||||
args = [_DEXDUMP_PATH] + dexfiles
|
||||
if file_summary:
|
||||
args.append('-f')
|
||||
|
||||
return cmd_helper.IterCmdOutputLines(args)
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# 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.
|
||||
|
||||
"""Manages intents and associated information.
|
||||
|
||||
This is generally intended to be used with functions that calls Android's
|
||||
Am command.
|
||||
"""
|
||||
|
||||
class Intent(object):
|
||||
|
||||
def __init__(self, action='android.intent.action.VIEW', activity=None,
|
||||
category=None, component=None, data=None, extras=None,
|
||||
flags=None, package=None):
|
||||
"""Creates an Intent.
|
||||
|
||||
Args:
|
||||
action: A string containing the action.
|
||||
activity: A string that, with |package|, can be used to specify the
|
||||
component.
|
||||
category: A string or list containing any categories.
|
||||
component: A string that specifies the component to send the intent to.
|
||||
data: A string containing a data URI.
|
||||
extras: A dict containing extra parameters to be passed along with the
|
||||
intent.
|
||||
flags: A string containing flags to pass.
|
||||
package: A string that, with activity, can be used to specify the
|
||||
component.
|
||||
"""
|
||||
self._action = action
|
||||
self._activity = activity
|
||||
if isinstance(category, list) or category is None:
|
||||
self._category = category
|
||||
else:
|
||||
self._category = [category]
|
||||
self._component = component
|
||||
self._data = data
|
||||
self._extras = extras
|
||||
self._flags = flags
|
||||
self._package = package
|
||||
|
||||
if self._component and '/' in component:
|
||||
self._package, self._activity = component.split('/', 1)
|
||||
elif self._package and self._activity:
|
||||
self._component = '%s/%s' % (package, activity)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
return self._action
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
return self._activity
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return self._category
|
||||
|
||||
@property
|
||||
def component(self):
|
||||
return self._component
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def extras(self):
|
||||
return self._extras
|
||||
|
||||
@property
|
||||
def flags(self):
|
||||
return self._flags
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
return self._package
|
||||
|
||||
@property
|
||||
def am_args(self):
|
||||
"""Returns the intent as a list of arguments for the activity manager.
|
||||
|
||||
For details refer to the specification at:
|
||||
- http://developer.android.com/tools/help/adb.html#IntentSpec
|
||||
"""
|
||||
args = []
|
||||
if self.action:
|
||||
args.extend(['-a', self.action])
|
||||
if self.data:
|
||||
args.extend(['-d', self.data])
|
||||
if self.category:
|
||||
args.extend(arg for cat in self.category for arg in ('-c', cat))
|
||||
if self.component:
|
||||
args.extend(['-n', self.component])
|
||||
if self.flags:
|
||||
args.extend(['-f', self.flags])
|
||||
if self.extras:
|
||||
for key, value in self.extras.iteritems():
|
||||
if value is None:
|
||||
args.extend(['--esn', key])
|
||||
elif isinstance(value, str):
|
||||
args.extend(['--es', key, value])
|
||||
elif isinstance(value, bool):
|
||||
args.extend(['--ez', key, str(value)])
|
||||
elif isinstance(value, int):
|
||||
args.extend(['--ei', key, str(value)])
|
||||
elif isinstance(value, float):
|
||||
args.extend(['--ef', key, str(value)])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Intent does not know how to pass %s extras' % type(value))
|
||||
return args
|
|
@ -0,0 +1,391 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""Helper object to read and modify Shared Preferences from Android apps.
|
||||
|
||||
See e.g.:
|
||||
http://developer.android.com/reference/android/content/SharedPreferences.html
|
||||
"""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import posixpath
|
||||
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
_XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
|
||||
|
||||
|
||||
class BasePref(object):
|
||||
"""Base class for getting/setting the value of a specific preference type.
|
||||
|
||||
Should not be instantiated directly. The SharedPrefs collection will
|
||||
instantiate the appropriate subclasses, which directly manipulate the
|
||||
underlying xml document, to parse and serialize values according to their
|
||||
type.
|
||||
|
||||
Args:
|
||||
elem: An xml ElementTree object holding the preference data.
|
||||
|
||||
Properties:
|
||||
tag_name: A string with the tag that must be used for this preference type.
|
||||
"""
|
||||
tag_name = None
|
||||
|
||||
def __init__(self, elem):
|
||||
if elem.tag != type(self).tag_name:
|
||||
raise TypeError('Property %r has type %r, but trying to access as %r' %
|
||||
(elem.get('name'), elem.tag, type(self).tag_name))
|
||||
self._elem = elem
|
||||
|
||||
def __str__(self):
|
||||
"""Get the underlying xml element as a string."""
|
||||
return ElementTree.tostring(self._elem)
|
||||
|
||||
def get(self):
|
||||
"""Get the value of this preference."""
|
||||
return self._elem.get('value')
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a value casted as a string."""
|
||||
self._elem.set('value', str(value))
|
||||
|
||||
@property
|
||||
def has_value(self):
|
||||
"""Check whether the element has a value."""
|
||||
return self._elem.get('value') is not None
|
||||
|
||||
|
||||
class BooleanPref(BasePref):
|
||||
"""Class for getting/setting a preference with a boolean value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<boolean name="featureEnabled" value="false" />
|
||||
"""
|
||||
tag_name = 'boolean'
|
||||
VALUES = {'true': True, 'false': False}
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python bool."""
|
||||
return type(self).VALUES[super(BooleanPref, self).get()]
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a value casted as a bool."""
|
||||
super(BooleanPref, self).set('true' if value else 'false')
|
||||
|
||||
|
||||
class FloatPref(BasePref):
|
||||
"""Class for getting/setting a preference with a float value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<float name="someMetric" value="4.7" />
|
||||
"""
|
||||
tag_name = 'float'
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python float."""
|
||||
return float(super(FloatPref, self).get())
|
||||
|
||||
|
||||
class IntPref(BasePref):
|
||||
"""Class for getting/setting a preference with an int value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<int name="aCounter" value="1234" />
|
||||
"""
|
||||
tag_name = 'int'
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python int."""
|
||||
return int(super(IntPref, self).get())
|
||||
|
||||
|
||||
class LongPref(IntPref):
|
||||
"""Class for getting/setting a preference with a long value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<long name="aLongCounter" value="1234" />
|
||||
|
||||
We use the same implementation from IntPref.
|
||||
"""
|
||||
tag_name = 'long'
|
||||
|
||||
|
||||
class StringPref(BasePref):
|
||||
"""Class for getting/setting a preference with a string value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<string name="someHashValue">249b3e5af13d4db2</string>
|
||||
"""
|
||||
tag_name = 'string'
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python string."""
|
||||
return self._elem.text
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a value casted as a string."""
|
||||
self._elem.text = str(value)
|
||||
|
||||
|
||||
class StringSetPref(StringPref):
|
||||
"""Class for getting/setting a preference with a set of string values.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<set name="managed_apps">
|
||||
<string>com.mine.app1</string>
|
||||
<string>com.mine.app2</string>
|
||||
<string>com.mine.app3</string>
|
||||
</set>
|
||||
"""
|
||||
tag_name = 'set'
|
||||
|
||||
def get(self):
|
||||
"""Get a list with the string values contained."""
|
||||
value = []
|
||||
for child in self._elem:
|
||||
assert child.tag == 'string'
|
||||
value.append(child.text)
|
||||
return value
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a sequence of values, each casted as a string."""
|
||||
for child in list(self._elem):
|
||||
self._elem.remove(child)
|
||||
for item in value:
|
||||
ElementTree.SubElement(self._elem, 'string').text = str(item)
|
||||
|
||||
|
||||
_PREF_TYPES = {c.tag_name: c for c in [BooleanPref, FloatPref, IntPref,
|
||||
LongPref, StringPref, StringSetPref]}
|
||||
|
||||
|
||||
class SharedPrefs(object):
|
||||
def __init__(self, device, package, filename):
|
||||
"""Helper object to read and update "Shared Prefs" of Android apps.
|
||||
|
||||
Such files typically look like, e.g.:
|
||||
|
||||
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
||||
<map>
|
||||
<int name="databaseVersion" value="107" />
|
||||
<boolean name="featureEnabled" value="false" />
|
||||
<string name="someHashValue">249b3e5af13d4db2</string>
|
||||
</map>
|
||||
|
||||
Example usage:
|
||||
|
||||
prefs = shared_prefs.SharedPrefs(device, 'com.my.app', 'my_prefs.xml')
|
||||
prefs.Load()
|
||||
prefs.GetString('someHashValue') # => '249b3e5af13d4db2'
|
||||
prefs.SetInt('databaseVersion', 42)
|
||||
prefs.Remove('featureEnabled')
|
||||
prefs.Commit()
|
||||
|
||||
The object may also be used as a context manager to automatically load and
|
||||
commit, respectively, upon entering and leaving the context.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils object.
|
||||
package: A string with the package name of the app that owns the shared
|
||||
preferences file.
|
||||
filename: A string with the name of the preferences file to read/write.
|
||||
"""
|
||||
self._device = device
|
||||
self._xml = None
|
||||
self._package = package
|
||||
self._filename = filename
|
||||
self._path = '/data/data/%s/shared_prefs/%s' % (package, filename)
|
||||
self._changed = False
|
||||
|
||||
def __repr__(self):
|
||||
"""Get a useful printable representation of the object."""
|
||||
return '<{cls} file {filename} for {package} on {device}>'.format(
|
||||
cls=type(self).__name__, filename=self.filename, package=self.package,
|
||||
device=str(self._device))
|
||||
|
||||
def __str__(self):
|
||||
"""Get the underlying xml document as a string."""
|
||||
return _XML_DECLARATION + ElementTree.tostring(self.xml)
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
"""Get the package name of the app that owns the shared preferences."""
|
||||
return self._package
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
"""Get the filename of the shared preferences file."""
|
||||
return self._filename
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Get the full path to the shared preferences file on the device."""
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""True if properties have changed and a commit would be needed."""
|
||||
return self._changed
|
||||
|
||||
@property
|
||||
def xml(self):
|
||||
"""Get the underlying xml document as an ElementTree object."""
|
||||
if self._xml is None:
|
||||
self._xml = ElementTree.Element('map')
|
||||
return self._xml
|
||||
|
||||
def Load(self):
|
||||
"""Load the shared preferences file from the device.
|
||||
|
||||
A empty xml document, which may be modified and saved on |commit|, is
|
||||
created if the file does not already exist.
|
||||
"""
|
||||
if self._device.FileExists(self.path):
|
||||
self._xml = ElementTree.fromstring(
|
||||
self._device.ReadFile(self.path, as_root=True))
|
||||
assert self._xml.tag == 'map'
|
||||
else:
|
||||
self._xml = None
|
||||
self._changed = False
|
||||
|
||||
def Clear(self):
|
||||
"""Clear all of the preferences contained in this object."""
|
||||
if self._xml is not None and len(self): # only clear if not already empty
|
||||
self._xml = None
|
||||
self._changed = True
|
||||
|
||||
def Commit(self):
|
||||
"""Save the current set of preferences to the device.
|
||||
|
||||
Only actually saves if some preferences have been modified.
|
||||
"""
|
||||
if not self.changed:
|
||||
return
|
||||
self._device.RunShellCommand(
|
||||
['mkdir', '-p', posixpath.dirname(self.path)],
|
||||
as_root=True, check_return=True)
|
||||
self._device.WriteFile(self.path, str(self), as_root=True)
|
||||
self._device.KillAll(self.package, exact=True, as_root=True, quiet=True)
|
||||
self._changed = False
|
||||
|
||||
def __len__(self):
|
||||
"""Get the number of preferences in this collection."""
|
||||
return len(self.xml)
|
||||
|
||||
def PropertyType(self, key):
|
||||
"""Get the type (i.e. tag name) of a property in the collection."""
|
||||
return self._GetChild(key).tag
|
||||
|
||||
def HasProperty(self, key):
|
||||
try:
|
||||
self._GetChild(key)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def GetBoolean(self, key):
|
||||
"""Get a boolean property."""
|
||||
return BooleanPref(self._GetChild(key)).get()
|
||||
|
||||
def SetBoolean(self, key, value):
|
||||
"""Set a boolean property."""
|
||||
self._SetPrefValue(key, value, BooleanPref)
|
||||
|
||||
def GetFloat(self, key):
|
||||
"""Get a float property."""
|
||||
return FloatPref(self._GetChild(key)).get()
|
||||
|
||||
def SetFloat(self, key, value):
|
||||
"""Set a float property."""
|
||||
self._SetPrefValue(key, value, FloatPref)
|
||||
|
||||
def GetInt(self, key):
|
||||
"""Get an int property."""
|
||||
return IntPref(self._GetChild(key)).get()
|
||||
|
||||
def SetInt(self, key, value):
|
||||
"""Set an int property."""
|
||||
self._SetPrefValue(key, value, IntPref)
|
||||
|
||||
def GetLong(self, key):
|
||||
"""Get a long property."""
|
||||
return LongPref(self._GetChild(key)).get()
|
||||
|
||||
def SetLong(self, key, value):
|
||||
"""Set a long property."""
|
||||
self._SetPrefValue(key, value, LongPref)
|
||||
|
||||
def GetString(self, key):
|
||||
"""Get a string property."""
|
||||
return StringPref(self._GetChild(key)).get()
|
||||
|
||||
def SetString(self, key, value):
|
||||
"""Set a string property."""
|
||||
self._SetPrefValue(key, value, StringPref)
|
||||
|
||||
def GetStringSet(self, key):
|
||||
"""Get a string set property."""
|
||||
return StringSetPref(self._GetChild(key)).get()
|
||||
|
||||
def SetStringSet(self, key, value):
|
||||
"""Set a string set property."""
|
||||
self._SetPrefValue(key, value, StringSetPref)
|
||||
|
||||
def Remove(self, key):
|
||||
"""Remove a preference from the collection."""
|
||||
self.xml.remove(self._GetChild(key))
|
||||
|
||||
def AsDict(self):
|
||||
"""Return the properties and their values as a dictionary."""
|
||||
d = {}
|
||||
for child in self.xml:
|
||||
pref = _PREF_TYPES[child.tag](child)
|
||||
d[child.get('name')] = pref.get()
|
||||
return d
|
||||
|
||||
def __enter__(self):
|
||||
"""Load preferences file from the device when entering a context."""
|
||||
self.Load()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, _exc_value, _traceback):
|
||||
"""Save preferences file to the device when leaving a context."""
|
||||
if not exc_type:
|
||||
self.Commit()
|
||||
|
||||
def _GetChild(self, key):
|
||||
"""Get the underlying xml node that holds the property of a given key.
|
||||
|
||||
Raises:
|
||||
KeyError when the key is not found in the collection.
|
||||
"""
|
||||
for child in self.xml:
|
||||
if child.get('name') == key:
|
||||
return child
|
||||
raise KeyError(key)
|
||||
|
||||
def _SetPrefValue(self, key, value, pref_cls):
|
||||
"""Set the value of a property.
|
||||
|
||||
Args:
|
||||
key: The key of the property to set.
|
||||
value: The new value of the property.
|
||||
pref_cls: A subclass of BasePref used to access the property.
|
||||
|
||||
Raises:
|
||||
TypeError when the key already exists but with a different type.
|
||||
"""
|
||||
try:
|
||||
pref = pref_cls(self._GetChild(key))
|
||||
old_value = pref.get()
|
||||
except KeyError:
|
||||
pref = pref_cls(ElementTree.SubElement(
|
||||
self.xml, pref_cls.tag_name, {'name': key}))
|
||||
old_value = None
|
||||
if old_value != value:
|
||||
pref.set(value)
|
||||
self._changed = True
|
||||
logging.info('Setting property: %s', pref)
|
|
@ -12,9 +12,9 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from devil.android import device_utils
|
||||
from devil.android.sdk import shared_prefs
|
||||
from pylib import constants
|
||||
from pylib.device import device_utils
|
||||
from pylib.device import shared_prefs
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright 2015 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.
|
||||
|
||||
"""This module wraps Android's split-select tool."""
|
||||
|
||||
import os
|
||||
|
||||
from devil.utils import cmd_helper
|
||||
from devil.utils import timeout_retry
|
||||
from pylib import constants
|
||||
|
||||
_SPLIT_SELECT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'split-select')
|
||||
|
||||
def _RunSplitSelectCmd(args):
|
||||
"""Runs a split-select command.
|
||||
|
||||
Args:
|
||||
args: A list of arguments for split-select.
|
||||
|
||||
Returns:
|
||||
The output of the command.
|
||||
"""
|
||||
cmd = [_SPLIT_SELECT_PATH] + args
|
||||
status, output = cmd_helper.GetCmdStatusAndOutput(cmd)
|
||||
if status != 0:
|
||||
raise Exception('Failed running command "%s" with output "%s".' %
|
||||
(' '.join(cmd), output))
|
||||
return output
|
||||
|
||||
def _SplitConfig(device):
|
||||
"""Returns a config specifying which APK splits are required by the device.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils object.
|
||||
"""
|
||||
return ('%s-r%s-%s:%s' %
|
||||
(device.language,
|
||||
device.country,
|
||||
device.screen_density,
|
||||
device.product_cpu_abi))
|
||||
|
||||
def SelectSplits(device, base_apk, split_apks):
|
||||
"""Determines which APK splits the device requires.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils object.
|
||||
base_apk: The path of the base APK.
|
||||
split_apks: A list of paths of APK splits.
|
||||
|
||||
Returns:
|
||||
The list of APK splits that the device requires.
|
||||
"""
|
||||
config = _SplitConfig(device)
|
||||
args = ['--target', config, '--base', base_apk]
|
||||
for split in split_apks:
|
||||
args.extend(['--split', split])
|
||||
return _RunSplitSelectCmd(args).splitlines()
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2015 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 BaseError(Exception):
|
||||
"""Base error for all test runner errors."""
|
||||
|
||||
def __init__(self, message, is_infra_error=False):
|
||||
super(BaseError, self).__init__(message)
|
||||
self._is_infra_error = is_infra_error
|
||||
|
||||
@property
|
||||
def is_infra_error(self):
|
||||
"""Property to indicate if error was caused by an infrastructure issue."""
|
||||
return self._is_infra_error
|
|
@ -0,0 +1,290 @@
|
|||
# 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.
|
||||
|
||||
"""A wrapper for subprocess to make calling shell commands easier."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pipes
|
||||
import select
|
||||
import signal
|
||||
import string
|
||||
import StringIO
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# fcntl is not available on Windows.
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
fcntl = None
|
||||
|
||||
_SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
|
||||
|
||||
def SingleQuote(s):
|
||||
"""Return an shell-escaped version of the string using single quotes.
|
||||
|
||||
Reliably quote a string which may contain unsafe characters (e.g. space,
|
||||
quote, or other special characters such as '$').
|
||||
|
||||
The returned value can be used in a shell command line as one token that gets
|
||||
to be interpreted literally.
|
||||
|
||||
Args:
|
||||
s: The string to quote.
|
||||
|
||||
Return:
|
||||
The string quoted using single quotes.
|
||||
"""
|
||||
return pipes.quote(s)
|
||||
|
||||
def DoubleQuote(s):
|
||||
"""Return an shell-escaped version of the string using double quotes.
|
||||
|
||||
Reliably quote a string which may contain unsafe characters (e.g. space
|
||||
or quote characters), while retaining some shell features such as variable
|
||||
interpolation.
|
||||
|
||||
The returned value can be used in a shell command line as one token that gets
|
||||
to be further interpreted by the shell.
|
||||
|
||||
The set of characters that retain their special meaning may depend on the
|
||||
shell implementation. This set usually includes: '$', '`', '\', '!', '*',
|
||||
and '@'.
|
||||
|
||||
Args:
|
||||
s: The string to quote.
|
||||
|
||||
Return:
|
||||
The string quoted using double quotes.
|
||||
"""
|
||||
if not s:
|
||||
return '""'
|
||||
elif all(c in _SafeShellChars for c in s):
|
||||
return s
|
||||
else:
|
||||
return '"' + s.replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
|
||||
return subprocess.Popen(
|
||||
args=args, cwd=cwd, stdout=stdout, stderr=stderr,
|
||||
shell=shell, close_fds=True, env=env,
|
||||
preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL))
|
||||
|
||||
|
||||
def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
|
||||
pipe = Popen(args, stdout=stdout, stderr=stderr, shell=shell, cwd=cwd,
|
||||
env=env)
|
||||
pipe.communicate()
|
||||
return pipe.wait()
|
||||
|
||||
|
||||
def RunCmd(args, cwd=None):
|
||||
"""Opens a subprocess to execute a program and returns its return value.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
|
||||
Returns:
|
||||
Return code from the command execution.
|
||||
"""
|
||||
logging.info(str(args) + ' ' + (cwd or ''))
|
||||
return Call(args, cwd=cwd)
|
||||
|
||||
|
||||
def GetCmdOutput(args, cwd=None, shell=False):
|
||||
"""Open a subprocess to execute a program and returns its output.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command.
|
||||
|
||||
Returns:
|
||||
Captures and returns the command's stdout.
|
||||
Prints the command's stderr to logger (which defaults to stdout).
|
||||
"""
|
||||
(_, output) = GetCmdStatusAndOutput(args, cwd, shell)
|
||||
return output
|
||||
|
||||
|
||||
def _ValidateAndLogCommand(args, cwd, shell):
|
||||
if isinstance(args, basestring):
|
||||
if not shell:
|
||||
raise Exception('string args must be run with shell=True')
|
||||
else:
|
||||
if shell:
|
||||
raise Exception('array args must be run with shell=False')
|
||||
args = ' '.join(SingleQuote(c) for c in args)
|
||||
if cwd is None:
|
||||
cwd = ''
|
||||
else:
|
||||
cwd = ':' + cwd
|
||||
logging.info('[host]%s> %s', cwd, args)
|
||||
return args
|
||||
|
||||
|
||||
def GetCmdStatusAndOutput(args, cwd=None, shell=False):
|
||||
"""Executes a subprocess and returns its exit code and output.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
|
||||
Returns:
|
||||
The 2-tuple (exit code, output).
|
||||
"""
|
||||
status, stdout, stderr = GetCmdStatusOutputAndError(
|
||||
args, cwd=cwd, shell=shell)
|
||||
|
||||
if stderr:
|
||||
logging.critical(stderr)
|
||||
if len(stdout) > 4096:
|
||||
logging.debug('Truncated output:')
|
||||
logging.debug(stdout[:4096])
|
||||
return (status, stdout)
|
||||
|
||||
def GetCmdStatusOutputAndError(args, cwd=None, shell=False):
|
||||
"""Executes a subprocess and returns its exit code, output, and errors.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
|
||||
Returns:
|
||||
The 2-tuple (exit code, output).
|
||||
"""
|
||||
_ValidateAndLogCommand(args, cwd, shell)
|
||||
pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
shell=shell, cwd=cwd)
|
||||
stdout, stderr = pipe.communicate()
|
||||
return (pipe.returncode, stdout, stderr)
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
"""Module-specific timeout exception."""
|
||||
|
||||
def __init__(self, output=None):
|
||||
super(TimeoutError, self).__init__()
|
||||
self._output = output
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
return self._output
|
||||
|
||||
|
||||
def _IterProcessStdout(process, timeout=None, buffer_size=4096,
|
||||
poll_interval=1):
|
||||
assert fcntl, 'fcntl module is required'
|
||||
try:
|
||||
# Enable non-blocking reads from the child's stdout.
|
||||
child_fd = process.stdout.fileno()
|
||||
fl = fcntl.fcntl(child_fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(child_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
|
||||
end_time = (time.time() + timeout) if timeout else None
|
||||
while True:
|
||||
if end_time and time.time() > end_time:
|
||||
raise TimeoutError()
|
||||
read_fds, _, _ = select.select([child_fd], [], [], poll_interval)
|
||||
if child_fd in read_fds:
|
||||
data = os.read(child_fd, buffer_size)
|
||||
if not data:
|
||||
break
|
||||
yield data
|
||||
if process.poll() is not None:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
# Make sure the process doesn't stick around if we fail with an
|
||||
# exception.
|
||||
process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
process.wait()
|
||||
|
||||
|
||||
def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
|
||||
logfile=None):
|
||||
"""Executes a subprocess with a timeout.
|
||||
|
||||
Args:
|
||||
args: List of arguments to the program, the program to execute is the first
|
||||
element.
|
||||
timeout: the timeout in seconds or None to wait forever.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
logfile: Optional file-like object that will receive output from the
|
||||
command as it is running.
|
||||
|
||||
Returns:
|
||||
The 2-tuple (exit code, output).
|
||||
"""
|
||||
_ValidateAndLogCommand(args, cwd, shell)
|
||||
output = StringIO.StringIO()
|
||||
process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
try:
|
||||
for data in _IterProcessStdout(process, timeout=timeout):
|
||||
if logfile:
|
||||
logfile.write(data)
|
||||
output.write(data)
|
||||
except TimeoutError:
|
||||
raise TimeoutError(output.getvalue())
|
||||
|
||||
return process.returncode, output.getvalue()
|
||||
|
||||
|
||||
def IterCmdOutputLines(args, timeout=None, cwd=None, shell=False,
|
||||
check_status=True):
|
||||
"""Executes a subprocess and continuously yields lines from its output.
|
||||
|
||||
Args:
|
||||
args: List of arguments to the program, the program to execute is the first
|
||||
element.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
check_status: A boolean indicating whether to check the exit status of the
|
||||
process after all output has been read.
|
||||
|
||||
Yields:
|
||||
The output of the subprocess, line by line.
|
||||
|
||||
Raises:
|
||||
CalledProcessError if check_status is True and the process exited with a
|
||||
non-zero exit status.
|
||||
"""
|
||||
cmd = _ValidateAndLogCommand(args, cwd, shell)
|
||||
process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
buffer_output = ''
|
||||
for data in _IterProcessStdout(process, timeout=timeout):
|
||||
buffer_output += data
|
||||
has_incomplete_line = buffer_output[-1] not in '\r\n'
|
||||
lines = buffer_output.splitlines()
|
||||
buffer_output = lines.pop() if has_incomplete_line else ''
|
||||
for line in lines:
|
||||
yield line
|
||||
if buffer_output:
|
||||
yield buffer_output
|
||||
if check_status and process.returncode:
|
||||
raise subprocess.CalledProcessError(process.returncode, cmd)
|
|
@ -7,7 +7,7 @@
|
|||
import unittest
|
||||
import subprocess
|
||||
|
||||
from pylib import cmd_helper
|
||||
from devil.utils import cmd_helper
|
||||
|
||||
|
||||
class CmdHelperSingleQuoteTest(unittest.TestCase):
|
|
@ -0,0 +1,16 @@
|
|||
# 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
|
||||
|
||||
|
||||
def GetRecursiveDiskUsage(path):
|
||||
"""Returns the disk usage in bytes of |path|. Similar to `du -sb |path|`."""
|
||||
running_size = os.path.getsize(path)
|
||||
if os.path.isdir(path):
|
||||
for root, dirs, files in os.walk(path):
|
||||
running_size += sum([os.path.getsize(os.path.join(root, f))
|
||||
for f in files + dirs])
|
||||
return running_size
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
A test facility to assert call sequences while mocking their behavior.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from pylib import constants
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
||||
import mock # pylint: disable=F0401
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""Adds assertCalls to TestCase objects."""
|
||||
class _AssertCalls(object):
|
||||
def __init__(self, test_case, expected_calls, watched):
|
||||
def call_action(pair):
|
||||
if isinstance(pair, type(mock.call)):
|
||||
return (pair, None)
|
||||
else:
|
||||
return pair
|
||||
|
||||
def do_check(call):
|
||||
def side_effect(*args, **kwargs):
|
||||
received_call = call(*args, **kwargs)
|
||||
self._test_case.assertTrue(
|
||||
self._expected_calls,
|
||||
msg=('Unexpected call: %s' % str(received_call)))
|
||||
expected_call, action = self._expected_calls.pop(0)
|
||||
self._test_case.assertTrue(
|
||||
received_call == expected_call,
|
||||
msg=('Expected call mismatch:\n'
|
||||
' expected: %s\n'
|
||||
' received: %s\n'
|
||||
% (str(expected_call), str(received_call))))
|
||||
if callable(action):
|
||||
return action(*args, **kwargs)
|
||||
else:
|
||||
return action
|
||||
return side_effect
|
||||
|
||||
self._test_case = test_case
|
||||
self._expected_calls = [call_action(pair) for pair in expected_calls]
|
||||
watched = watched.copy() # do not pollute the caller's dict
|
||||
watched.update((call.parent.name, call.parent)
|
||||
for call, _ in self._expected_calls)
|
||||
self._patched = [test_case.patch_call(call, side_effect=do_check(call))
|
||||
for call in watched.itervalues()]
|
||||
|
||||
def __enter__(self):
|
||||
for patch in self._patched:
|
||||
patch.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
for patch in self._patched:
|
||||
patch.__exit__(exc_type, exc_val, exc_tb)
|
||||
if exc_type is None:
|
||||
missing = ''.join(' expected: %s\n' % str(call)
|
||||
for call, _ in self._expected_calls)
|
||||
self._test_case.assertFalse(
|
||||
missing,
|
||||
msg='Expected calls not found:\n' + missing)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestCase, self).__init__(*args, **kwargs)
|
||||
self.call = mock.call.self
|
||||
self._watched = {}
|
||||
|
||||
def call_target(self, call):
|
||||
"""Resolve a self.call instance to the target it represents.
|
||||
|
||||
Args:
|
||||
call: a self.call instance, e.g. self.call.adb.Shell
|
||||
|
||||
Returns:
|
||||
The target object represented by the call, e.g. self.adb.Shell
|
||||
|
||||
Raises:
|
||||
ValueError if the path of the call does not start with "self", i.e. the
|
||||
target of the call is external to the self object.
|
||||
AttributeError if the path of the call does not specify a valid
|
||||
chain of attributes (without any calls) starting from "self".
|
||||
"""
|
||||
path = call.name.split('.')
|
||||
if path.pop(0) != 'self':
|
||||
raise ValueError("Target %r outside of 'self' object" % call.name)
|
||||
target = self
|
||||
for attr in path:
|
||||
target = getattr(target, attr)
|
||||
return target
|
||||
|
||||
def patch_call(self, call, **kwargs):
|
||||
"""Patch the target of a mock.call instance.
|
||||
|
||||
Args:
|
||||
call: a mock.call instance identifying a target to patch
|
||||
Extra keyword arguments are processed by mock.patch
|
||||
|
||||
Returns:
|
||||
A context manager to mock/unmock the target of the call
|
||||
"""
|
||||
if call.name.startswith('self.'):
|
||||
target = self.call_target(call.parent)
|
||||
_, attribute = call.name.rsplit('.', 1)
|
||||
if (hasattr(type(target), attribute)
|
||||
and isinstance(getattr(type(target), attribute), property)):
|
||||
return mock.patch.object(
|
||||
type(target), attribute, new_callable=mock.PropertyMock, **kwargs)
|
||||
else:
|
||||
return mock.patch.object(target, attribute, **kwargs)
|
||||
else:
|
||||
return mock.patch(call.name, **kwargs)
|
||||
|
||||
def watchCalls(self, calls):
|
||||
"""Add calls to the set of watched calls.
|
||||
|
||||
Args:
|
||||
calls: a sequence of mock.call instances identifying targets to watch
|
||||
"""
|
||||
self._watched.update((call.name, call) for call in calls)
|
||||
|
||||
def watchMethodCalls(self, call, ignore=None):
|
||||
"""Watch all public methods of the target identified by a self.call.
|
||||
|
||||
Args:
|
||||
call: a self.call instance indetifying an object
|
||||
ignore: a list of public methods to ignore when watching for calls
|
||||
"""
|
||||
target = self.call_target(call)
|
||||
if ignore is None:
|
||||
ignore = []
|
||||
self.watchCalls(getattr(call, method)
|
||||
for method in dir(target.__class__)
|
||||
if not method.startswith('_') and not method in ignore)
|
||||
|
||||
def clearWatched(self):
|
||||
"""Clear the set of watched calls."""
|
||||
self._watched = {}
|
||||
|
||||
def assertCalls(self, *calls):
|
||||
"""A context manager to assert that a sequence of calls is made.
|
||||
|
||||
During the assertion, a number of functions and methods will be "watched",
|
||||
and any calls made to them is expected to appear---in the exact same order,
|
||||
and with the exact same arguments---as specified by the argument |calls|.
|
||||
|
||||
By default, the targets of all expected calls are watched. Further targets
|
||||
to watch may be added using watchCalls and watchMethodCalls.
|
||||
|
||||
Optionaly, each call may be accompanied by an action. If the action is a
|
||||
(non-callable) value, this value will be used as the return value given to
|
||||
the caller when the matching call is found. Alternatively, if the action is
|
||||
a callable, the action will be then called with the same arguments as the
|
||||
intercepted call, so that it can provide a return value or perform other
|
||||
side effects. If the action is missing, a return value of None is assumed.
|
||||
|
||||
Note that mock.Mock objects are often convenient to use as a callable
|
||||
action, e.g. to raise exceptions or return other objects which are
|
||||
themselves callable.
|
||||
|
||||
Args:
|
||||
calls: each argument is either a pair (expected_call, action) or just an
|
||||
expected_call, where expected_call is a mock.call instance.
|
||||
|
||||
Raises:
|
||||
AssertionError if the watched targets do not receive the exact sequence
|
||||
of calls specified. Missing calls, extra calls, and calls with
|
||||
mismatching arguments, all cause the assertion to fail.
|
||||
"""
|
||||
return self._AssertCalls(self, calls, self._watched)
|
||||
|
||||
def assertCall(self, call, action=None):
|
||||
return self.assertCalls((call, action))
|
||||
|
|
@ -12,8 +12,8 @@ import os
|
|||
import sys
|
||||
import unittest
|
||||
|
||||
from devil.utils import mock_calls
|
||||
from pylib import constants
|
||||
from pylib.utils import mock_calls
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
|
@ -0,0 +1,242 @@
|
|||
# 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.
|
||||
|
||||
""" Wrapper that allows method execution in parallel.
|
||||
|
||||
This class wraps a list of objects of the same type, emulates their
|
||||
interface, and executes any functions called on the objects in parallel
|
||||
in ReraiserThreads.
|
||||
|
||||
This means that, given a list of objects:
|
||||
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
self.baz = Baz()
|
||||
|
||||
def bar(self, my_param):
|
||||
// do something
|
||||
|
||||
list_of_foos = [Foo(1), Foo(2), Foo(3)]
|
||||
|
||||
we can take a sequential operation on that list of objects:
|
||||
|
||||
for f in list_of_foos:
|
||||
f.bar('Hello')
|
||||
|
||||
and run it in parallel across all of the objects:
|
||||
|
||||
Parallelizer(list_of_foos).bar('Hello')
|
||||
|
||||
It can also handle (non-method) attributes of objects, so that this:
|
||||
|
||||
for f in list_of_foos:
|
||||
f.baz.myBazMethod()
|
||||
|
||||
can be run in parallel with:
|
||||
|
||||
Parallelizer(list_of_foos).baz.myBazMethod()
|
||||
|
||||
Because it emulates the interface of the wrapped objects, a Parallelizer
|
||||
can be passed to a method or function that takes objects of that type:
|
||||
|
||||
def DoesSomethingWithFoo(the_foo):
|
||||
the_foo.bar('Hello')
|
||||
the_foo.bar('world')
|
||||
the_foo.baz.myBazMethod
|
||||
|
||||
DoesSomethingWithFoo(Parallelizer(list_of_foos))
|
||||
|
||||
Note that this class spins up a thread for each object. Using this class
|
||||
to parallelize operations that are already fast will incur a net performance
|
||||
penalty.
|
||||
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
from devil.utils import reraiser_thread
|
||||
from devil.utils import watchdog_timer
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 3
|
||||
|
||||
|
||||
class Parallelizer(object):
|
||||
"""Allows parallel execution of method calls across a group of objects."""
|
||||
|
||||
def __init__(self, objs):
|
||||
assert (objs is not None and len(objs) > 0), (
|
||||
"Passed empty list to 'Parallelizer'")
|
||||
self._orig_objs = objs
|
||||
self._objs = objs
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Emulate getting the |name| attribute of |self|.
|
||||
|
||||
Args:
|
||||
name: The name of the attribute to retrieve.
|
||||
Returns:
|
||||
A Parallelizer emulating the |name| attribute of |self|.
|
||||
"""
|
||||
self.pGet(None)
|
||||
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = [getattr(o, name) for o in self._objs]
|
||||
return r
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Emulate getting the value of |self| at |index|.
|
||||
|
||||
Returns:
|
||||
A Parallelizer emulating the value of |self| at |index|.
|
||||
"""
|
||||
self.pGet(None)
|
||||
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = [o[index] for o in self._objs]
|
||||
return r
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Emulate calling |self| with |args| and |kwargs|.
|
||||
|
||||
Note that this call is asynchronous. Call pFinish on the return value to
|
||||
block until the call finishes.
|
||||
|
||||
Returns:
|
||||
A Parallelizer wrapping the ReraiserThreadGroup running the call in
|
||||
parallel.
|
||||
Raises:
|
||||
AttributeError if the wrapped objects aren't callable.
|
||||
"""
|
||||
self.pGet(None)
|
||||
|
||||
if not self._objs:
|
||||
raise AttributeError('Nothing to call.')
|
||||
for o in self._objs:
|
||||
if not callable(o):
|
||||
raise AttributeError("'%s' is not callable" % o.__name__)
|
||||
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = reraiser_thread.ReraiserThreadGroup(
|
||||
[reraiser_thread.ReraiserThread(
|
||||
o, args=args, kwargs=kwargs,
|
||||
name='%s.%s' % (str(d), o.__name__))
|
||||
for d, o in zip(self._orig_objs, self._objs)])
|
||||
r._objs.StartAll() # pylint: disable=W0212
|
||||
return r
|
||||
|
||||
def pFinish(self, timeout):
|
||||
"""Finish any outstanding asynchronous operations.
|
||||
|
||||
Args:
|
||||
timeout: The maximum number of seconds to wait for an individual
|
||||
result to return, or None to wait forever.
|
||||
Returns:
|
||||
self, now emulating the return values.
|
||||
"""
|
||||
self._assertNoShadow('pFinish')
|
||||
if isinstance(self._objs, reraiser_thread.ReraiserThreadGroup):
|
||||
self._objs.JoinAll()
|
||||
self._objs = self._objs.GetAllReturnValues(
|
||||
watchdog_timer.WatchdogTimer(timeout))
|
||||
return self
|
||||
|
||||
def pGet(self, timeout):
|
||||
"""Get the current wrapped objects.
|
||||
|
||||
Args:
|
||||
timeout: Same as |pFinish|.
|
||||
Returns:
|
||||
A list of the results, in order of the provided devices.
|
||||
Raises:
|
||||
Any exception raised by any of the called functions.
|
||||
"""
|
||||
self._assertNoShadow('pGet')
|
||||
self.pFinish(timeout)
|
||||
return self._objs
|
||||
|
||||
def pMap(self, f, *args, **kwargs):
|
||||
"""Map a function across the current wrapped objects in parallel.
|
||||
|
||||
This calls f(o, *args, **kwargs) for each o in the set of wrapped objects.
|
||||
|
||||
Note that this call is asynchronous. Call pFinish on the return value to
|
||||
block until the call finishes.
|
||||
|
||||
Args:
|
||||
f: The function to call.
|
||||
args: The positional args to pass to f.
|
||||
kwargs: The keyword args to pass to f.
|
||||
Returns:
|
||||
A Parallelizer wrapping the ReraiserThreadGroup running the map in
|
||||
parallel.
|
||||
"""
|
||||
self._assertNoShadow('pMap')
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = reraiser_thread.ReraiserThreadGroup(
|
||||
[reraiser_thread.ReraiserThread(
|
||||
f, args=tuple([o] + list(args)), kwargs=kwargs,
|
||||
name='%s(%s)' % (f.__name__, d))
|
||||
for d, o in zip(self._orig_objs, self._objs)])
|
||||
r._objs.StartAll() # pylint: disable=W0212
|
||||
return r
|
||||
|
||||
def _assertNoShadow(self, attr_name):
|
||||
"""Ensures that |attr_name| isn't shadowing part of the wrapped obejcts.
|
||||
|
||||
If the wrapped objects _do_ have an |attr_name| attribute, it will be
|
||||
inaccessible to clients.
|
||||
|
||||
Args:
|
||||
attr_name: The attribute to check.
|
||||
Raises:
|
||||
AssertionError if the wrapped objects have an attribute named 'attr_name'
|
||||
or '_assertNoShadow'.
|
||||
"""
|
||||
if isinstance(self._objs, reraiser_thread.ReraiserThreadGroup):
|
||||
assert not hasattr(self._objs, '_assertNoShadow')
|
||||
assert not hasattr(self._objs, attr_name)
|
||||
else:
|
||||
assert not any(hasattr(o, '_assertNoShadow') for o in self._objs)
|
||||
assert not any(hasattr(o, attr_name) for o in self._objs)
|
||||
|
||||
|
||||
class SyncParallelizer(Parallelizer):
|
||||
"""A Parallelizer that blocks on function calls."""
|
||||
|
||||
#override
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Emulate calling |self| with |args| and |kwargs|.
|
||||
|
||||
Note that this call is synchronous.
|
||||
|
||||
Returns:
|
||||
A Parallelizer emulating the value returned from calling |self| with
|
||||
|args| and |kwargs|.
|
||||
Raises:
|
||||
AttributeError if the wrapped objects aren't callable.
|
||||
"""
|
||||
r = super(SyncParallelizer, self).__call__(*args, **kwargs)
|
||||
r.pFinish(None)
|
||||
return r
|
||||
|
||||
#override
|
||||
def pMap(self, f, *args, **kwargs):
|
||||
"""Map a function across the current wrapped objects in parallel.
|
||||
|
||||
This calls f(o, *args, **kwargs) for each o in the set of wrapped objects.
|
||||
|
||||
Note that this call is synchronous.
|
||||
|
||||
Args:
|
||||
f: The function to call.
|
||||
args: The positional args to pass to f.
|
||||
kwargs: The keyword args to pass to f.
|
||||
Returns:
|
||||
A Parallelizer wrapping the ReraiserThreadGroup running the map in
|
||||
parallel.
|
||||
"""
|
||||
r = super(SyncParallelizer, self).pMap(f, *args, **kwargs)
|
||||
r.pFinish(None)
|
||||
return r
|
||||
|
|
@ -12,7 +12,7 @@ import tempfile
|
|||
import time
|
||||
import unittest
|
||||
|
||||
from pylib.utils import parallelizer
|
||||
from devil.utils import parallelizer
|
||||
|
||||
|
||||
class ParallelizerTestObject(object):
|
|
@ -0,0 +1,158 @@
|
|||
# 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.
|
||||
|
||||
"""Thread and ThreadGroup that reraise exceptions on the main thread."""
|
||||
# pylint: disable=W0212
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from devil.utils import watchdog_timer
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
"""Module-specific timeout exception."""
|
||||
pass
|
||||
|
||||
|
||||
def LogThreadStack(thread):
|
||||
"""Log the stack for the given thread.
|
||||
|
||||
Args:
|
||||
thread: a threading.Thread instance.
|
||||
"""
|
||||
stack = sys._current_frames()[thread.ident]
|
||||
logging.critical('*' * 80)
|
||||
logging.critical('Stack dump for thread %r', thread.name)
|
||||
logging.critical('*' * 80)
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
logging.critical('File: "%s", line %d, in %s', filename, lineno, name)
|
||||
if line:
|
||||
logging.critical(' %s', line.strip())
|
||||
logging.critical('*' * 80)
|
||||
|
||||
|
||||
class ReraiserThread(threading.Thread):
|
||||
"""Thread class that can reraise exceptions."""
|
||||
|
||||
def __init__(self, func, args=None, kwargs=None, name=None):
|
||||
"""Initialize thread.
|
||||
|
||||
Args:
|
||||
func: callable to call on a new thread.
|
||||
args: list of positional arguments for callable, defaults to empty.
|
||||
kwargs: dictionary of keyword arguments for callable, defaults to empty.
|
||||
name: thread name, defaults to Thread-N.
|
||||
"""
|
||||
super(ReraiserThread, self).__init__(name=name)
|
||||
if not args:
|
||||
args = []
|
||||
if not kwargs:
|
||||
kwargs = {}
|
||||
self.daemon = True
|
||||
self._func = func
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._ret = None
|
||||
self._exc_info = None
|
||||
|
||||
def ReraiseIfException(self):
|
||||
"""Reraise exception if an exception was raised in the thread."""
|
||||
if self._exc_info:
|
||||
raise self._exc_info[0], self._exc_info[1], self._exc_info[2]
|
||||
|
||||
def GetReturnValue(self):
|
||||
"""Reraise exception if present, otherwise get the return value."""
|
||||
self.ReraiseIfException()
|
||||
return self._ret
|
||||
|
||||
#override
|
||||
def run(self):
|
||||
"""Overrides Thread.run() to add support for reraising exceptions."""
|
||||
try:
|
||||
self._ret = self._func(*self._args, **self._kwargs)
|
||||
except: # pylint: disable=W0702
|
||||
self._exc_info = sys.exc_info()
|
||||
|
||||
|
||||
class ReraiserThreadGroup(object):
|
||||
"""A group of ReraiserThread objects."""
|
||||
|
||||
def __init__(self, threads=None):
|
||||
"""Initialize thread group.
|
||||
|
||||
Args:
|
||||
threads: a list of ReraiserThread objects; defaults to empty.
|
||||
"""
|
||||
if not threads:
|
||||
threads = []
|
||||
self._threads = threads
|
||||
|
||||
def Add(self, thread):
|
||||
"""Add a thread to the group.
|
||||
|
||||
Args:
|
||||
thread: a ReraiserThread object.
|
||||
"""
|
||||
self._threads.append(thread)
|
||||
|
||||
def StartAll(self):
|
||||
"""Start all threads."""
|
||||
for thread in self._threads:
|
||||
thread.start()
|
||||
|
||||
def _JoinAll(self, watcher=None):
|
||||
"""Join all threads without stack dumps.
|
||||
|
||||
Reraises exceptions raised by the child threads and supports breaking
|
||||
immediately on exceptions raised on the main thread.
|
||||
|
||||
Args:
|
||||
watcher: Watchdog object providing timeout, by default waits forever.
|
||||
"""
|
||||
if watcher is None:
|
||||
watcher = watchdog_timer.WatchdogTimer(None)
|
||||
alive_threads = self._threads[:]
|
||||
while alive_threads:
|
||||
for thread in alive_threads[:]:
|
||||
if watcher.IsTimedOut():
|
||||
raise TimeoutError('Timed out waiting for %d of %d threads.' %
|
||||
(len(alive_threads), len(self._threads)))
|
||||
# Allow the main thread to periodically check for interrupts.
|
||||
thread.join(0.1)
|
||||
if not thread.isAlive():
|
||||
alive_threads.remove(thread)
|
||||
# All threads are allowed to complete before reraising exceptions.
|
||||
for thread in self._threads:
|
||||
thread.ReraiseIfException()
|
||||
|
||||
def JoinAll(self, watcher=None):
|
||||
"""Join all threads.
|
||||
|
||||
Reraises exceptions raised by the child threads and supports breaking
|
||||
immediately on exceptions raised on the main thread. Unfinished threads'
|
||||
stacks will be logged on watchdog timeout.
|
||||
|
||||
Args:
|
||||
watcher: Watchdog object providing timeout, by default waits forever.
|
||||
"""
|
||||
try:
|
||||
self._JoinAll(watcher)
|
||||
except TimeoutError:
|
||||
for thread in (t for t in self._threads if t.isAlive()):
|
||||
LogThreadStack(thread)
|
||||
raise
|
||||
|
||||
def GetAllReturnValues(self, watcher=None):
|
||||
"""Get all return values, joining all threads if necessary.
|
||||
|
||||
Args:
|
||||
watcher: same as in |JoinAll|. Only used if threads are alive.
|
||||
"""
|
||||
if any([t.isAlive() for t in self._threads]):
|
||||
self.JoinAll(watcher)
|
||||
return [t.GetReturnValue() for t in self._threads]
|
||||
|
|
@ -9,10 +9,10 @@ import logging
|
|||
import re
|
||||
import sys
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import device_errors
|
||||
from pylib.utils import run_tests_helper
|
||||
from devil.android import device_errors
|
||||
from devil.android.sdk import adb_wrapper
|
||||
from devil.utils import cmd_helper
|
||||
from devil.utils import run_tests_helper
|
||||
|
||||
_INDENTATION_RE = re.compile(r'^( *)')
|
||||
_LSUSB_BUS_DEVICE_RE = re.compile(r'^Bus (\d{3}) Device (\d{3}):')
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# 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.
|
||||
|
||||
"""Helper functions common to native, java and host-driven test runners."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
"""Custom log formatter."""
|
||||
|
||||
#override
|
||||
def __init__(self, fmt='%(threadName)-4s %(message)s'):
|
||||
# Can't use super() because in older Python versions logging.Formatter does
|
||||
# not inherit from object.
|
||||
logging.Formatter.__init__(self, fmt=fmt)
|
||||
self._creation_time = time.time()
|
||||
|
||||
#override
|
||||
def format(self, record):
|
||||
# Can't use super() because in older Python versions logging.Formatter does
|
||||
# not inherit from object.
|
||||
msg = logging.Formatter.format(self, record)
|
||||
if 'MainThread' in msg[:19]:
|
||||
msg = msg.replace('MainThread', 'Main', 1)
|
||||
timediff = time.time() - self._creation_time
|
||||
return '%s %8.3fs %s' % (record.levelname[0], timediff, msg)
|
||||
|
||||
|
||||
def SetLogLevel(verbose_count):
|
||||
"""Sets log level as |verbose_count|."""
|
||||
log_level = logging.WARNING # Default.
|
||||
if verbose_count == 1:
|
||||
log_level = logging.INFO
|
||||
elif verbose_count >= 2:
|
||||
log_level = logging.DEBUG
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(log_level)
|
||||
custom_handler = logging.StreamHandler(sys.stdout)
|
||||
custom_handler.setFormatter(CustomFormatter())
|
||||
logging.getLogger().addHandler(custom_handler)
|
|
@ -0,0 +1,167 @@
|
|||
# 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.
|
||||
|
||||
"""A utility to run functions with timeouts and retries."""
|
||||
# pylint: disable=W0702
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from devil.utils import reraiser_thread
|
||||
from devil.utils import watchdog_timer
|
||||
|
||||
|
||||
class TimeoutRetryThread(reraiser_thread.ReraiserThread):
|
||||
def __init__(self, func, timeout, name):
|
||||
super(TimeoutRetryThread, self).__init__(func, name=name)
|
||||
self._watcher = watchdog_timer.WatchdogTimer(timeout)
|
||||
self._expired = False
|
||||
|
||||
def GetWatcher(self):
|
||||
"""Returns the watchdog keeping track of this thread's time."""
|
||||
return self._watcher
|
||||
|
||||
def GetElapsedTime(self):
|
||||
return self._watcher.GetElapsed()
|
||||
|
||||
def GetRemainingTime(self, required=0, msg=None):
|
||||
"""Get the remaining time before the thread times out.
|
||||
|
||||
Useful to send as the |timeout| parameter of async IO operations.
|
||||
|
||||
Args:
|
||||
required: minimum amount of time that will be required to complete, e.g.,
|
||||
some sleep or IO operation.
|
||||
msg: error message to show if timing out.
|
||||
|
||||
Returns:
|
||||
The number of seconds remaining before the thread times out, or None
|
||||
if the thread never times out.
|
||||
|
||||
Raises:
|
||||
reraiser_thread.TimeoutError if the remaining time is less than the
|
||||
required time.
|
||||
"""
|
||||
remaining = self._watcher.GetRemaining()
|
||||
if remaining is not None and remaining < required:
|
||||
if msg is None:
|
||||
msg = 'Timeout expired'
|
||||
if remaining > 0:
|
||||
msg += (', wait of %.1f secs required but only %.1f secs left'
|
||||
% (required, remaining))
|
||||
self._expired = True
|
||||
raise reraiser_thread.TimeoutError(msg)
|
||||
return remaining
|
||||
|
||||
def LogTimeoutException(self):
|
||||
"""Log the exception that terminated this thread."""
|
||||
if not self._expired:
|
||||
return
|
||||
logging.critical('*' * 80)
|
||||
logging.critical('%s on thread %r', self._exc_info[0].__name__, self.name)
|
||||
logging.critical('*' * 80)
|
||||
fmt_exc = ''.join(traceback.format_exception(*self._exc_info))
|
||||
for line in fmt_exc.splitlines():
|
||||
logging.critical(line.rstrip())
|
||||
logging.critical('*' * 80)
|
||||
|
||||
|
||||
def CurrentTimeoutThread():
|
||||
"""Get the current thread if it is a TimeoutRetryThread.
|
||||
|
||||
Returns:
|
||||
The current thread if it is a TimeoutRetryThread, otherwise None.
|
||||
"""
|
||||
current_thread = threading.current_thread()
|
||||
if isinstance(current_thread, TimeoutRetryThread):
|
||||
return current_thread
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def WaitFor(condition, wait_period=5, max_tries=None):
|
||||
"""Wait for a condition to become true.
|
||||
|
||||
Repeadly call the function condition(), with no arguments, until it returns
|
||||
a true value.
|
||||
|
||||
If called within a TimeoutRetryThread, it cooperates nicely with it.
|
||||
|
||||
Args:
|
||||
condition: function with the condition to check
|
||||
wait_period: number of seconds to wait before retrying to check the
|
||||
condition
|
||||
max_tries: maximum number of checks to make, the default tries forever
|
||||
or until the TimeoutRetryThread expires.
|
||||
|
||||
Returns:
|
||||
The true value returned by the condition, or None if the condition was
|
||||
not met after max_tries.
|
||||
|
||||
Raises:
|
||||
reraiser_thread.TimeoutError if the current thread is a TimeoutRetryThread
|
||||
and the timeout expires.
|
||||
"""
|
||||
condition_name = condition.__name__
|
||||
timeout_thread = CurrentTimeoutThread()
|
||||
while max_tries is None or max_tries > 0:
|
||||
result = condition()
|
||||
if max_tries is not None:
|
||||
max_tries -= 1
|
||||
msg = ['condition', repr(condition_name), 'met' if result else 'not met']
|
||||
if timeout_thread:
|
||||
msg.append('(%.1fs)' % timeout_thread.GetElapsedTime())
|
||||
logging.info(' '.join(msg))
|
||||
if result:
|
||||
return result
|
||||
if timeout_thread:
|
||||
timeout_thread.GetRemainingTime(wait_period,
|
||||
msg='Timed out waiting for %r' % condition_name)
|
||||
time.sleep(wait_period)
|
||||
return None
|
||||
|
||||
|
||||
def Run(func, timeout, retries, args=None, kwargs=None):
|
||||
"""Runs the passed function in a separate thread with timeouts and retries.
|
||||
|
||||
Args:
|
||||
func: the function to be wrapped.
|
||||
timeout: the timeout in seconds for each try.
|
||||
retries: the number of retries.
|
||||
args: list of positional args to pass to |func|.
|
||||
kwargs: dictionary of keyword args to pass to |func|.
|
||||
|
||||
Returns:
|
||||
The return value of func(*args, **kwargs).
|
||||
"""
|
||||
if not args:
|
||||
args = []
|
||||
if not kwargs:
|
||||
kwargs = {}
|
||||
|
||||
# The return value uses a list because Python variables are references, not
|
||||
# values. Closures make a copy of the reference, so updating the closure's
|
||||
# reference wouldn't update where the original reference pointed.
|
||||
ret = [None]
|
||||
def RunOnTimeoutThread():
|
||||
ret[0] = func(*args, **kwargs)
|
||||
|
||||
num_try = 1
|
||||
while True:
|
||||
child_thread = TimeoutRetryThread(
|
||||
RunOnTimeoutThread, timeout,
|
||||
name='TimeoutThread-%d-for-%s' % (num_try,
|
||||
threading.current_thread().name))
|
||||
try:
|
||||
thread_group = reraiser_thread.ReraiserThreadGroup([child_thread])
|
||||
thread_group.StartAll()
|
||||
thread_group.JoinAll(child_thread.GetWatcher())
|
||||
return ret[0]
|
||||
except:
|
||||
child_thread.LogTimeoutException()
|
||||
if num_try > retries:
|
||||
raise
|
||||
num_try += 1
|
|
@ -0,0 +1,47 @@
|
|||
# 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.
|
||||
|
||||
"""WatchdogTimer timeout objects."""
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class WatchdogTimer(object):
|
||||
"""A resetable timeout-based watchdog.
|
||||
|
||||
This object is threadsafe.
|
||||
"""
|
||||
|
||||
def __init__(self, timeout):
|
||||
"""Initializes the watchdog.
|
||||
|
||||
Args:
|
||||
timeout: The timeout in seconds. If timeout is None it will never timeout.
|
||||
"""
|
||||
self._start_time = time.time()
|
||||
self._timeout = timeout
|
||||
|
||||
def Reset(self):
|
||||
"""Resets the timeout countdown."""
|
||||
self._start_time = time.time()
|
||||
|
||||
def GetElapsed(self):
|
||||
"""Returns the elapsed time of the watchdog."""
|
||||
return time.time() - self._start_time
|
||||
|
||||
def GetRemaining(self):
|
||||
"""Returns the remaining time of the watchdog."""
|
||||
if self._timeout:
|
||||
return self._timeout - self.GetElapsed()
|
||||
else:
|
||||
return None
|
||||
|
||||
def IsTimedOut(self):
|
||||
"""Whether the watchdog has timed out.
|
||||
|
||||
Returns:
|
||||
True if the watchdog has timed out, False otherwise.
|
||||
"""
|
||||
remaining = self.GetRemaining()
|
||||
return remaining is not None and remaining < 0
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright 2015 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 logging
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
|
||||
def WriteToZipFile(zip_file, path, arc_path):
|
||||
"""Recursively write |path| to |zip_file| as |arc_path|.
|
||||
|
||||
zip_file: An open instance of zipfile.ZipFile.
|
||||
path: An absolute path to the file or directory to be zipped.
|
||||
arc_path: A relative path within the zip file to which the file or directory
|
||||
located at |path| should be written.
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
for dir_path, _, file_names in os.walk(path):
|
||||
dir_arc_path = os.path.join(arc_path, os.path.relpath(dir_path, path))
|
||||
logging.debug('dir: %s -> %s', dir_path, dir_arc_path)
|
||||
zip_file.write(dir_path, dir_arc_path, zipfile.ZIP_STORED)
|
||||
for f in file_names:
|
||||
file_path = os.path.join(dir_path, f)
|
||||
file_arc_path = os.path.join(dir_arc_path, f)
|
||||
logging.debug('file: %s -> %s', file_path, file_arc_path)
|
||||
zip_file.write(file_path, file_arc_path, zipfile.ZIP_DEFLATED)
|
||||
else:
|
||||
logging.debug('file: %s -> %s', path, arc_path)
|
||||
zip_file.write(path, arc_path, zipfile.ZIP_DEFLATED)
|
||||
|
|
@ -1,290 +1,8 @@
|
|||
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""A wrapper for subprocess to make calling shell commands easier."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pipes
|
||||
import select
|
||||
import signal
|
||||
import string
|
||||
import StringIO
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# fcntl is not available on Windows.
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
fcntl = None
|
||||
|
||||
_SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
|
||||
|
||||
def SingleQuote(s):
|
||||
"""Return an shell-escaped version of the string using single quotes.
|
||||
|
||||
Reliably quote a string which may contain unsafe characters (e.g. space,
|
||||
quote, or other special characters such as '$').
|
||||
|
||||
The returned value can be used in a shell command line as one token that gets
|
||||
to be interpreted literally.
|
||||
|
||||
Args:
|
||||
s: The string to quote.
|
||||
|
||||
Return:
|
||||
The string quoted using single quotes.
|
||||
"""
|
||||
return pipes.quote(s)
|
||||
|
||||
def DoubleQuote(s):
|
||||
"""Return an shell-escaped version of the string using double quotes.
|
||||
|
||||
Reliably quote a string which may contain unsafe characters (e.g. space
|
||||
or quote characters), while retaining some shell features such as variable
|
||||
interpolation.
|
||||
|
||||
The returned value can be used in a shell command line as one token that gets
|
||||
to be further interpreted by the shell.
|
||||
|
||||
The set of characters that retain their special meaning may depend on the
|
||||
shell implementation. This set usually includes: '$', '`', '\', '!', '*',
|
||||
and '@'.
|
||||
|
||||
Args:
|
||||
s: The string to quote.
|
||||
|
||||
Return:
|
||||
The string quoted using double quotes.
|
||||
"""
|
||||
if not s:
|
||||
return '""'
|
||||
elif all(c in _SafeShellChars for c in s):
|
||||
return s
|
||||
else:
|
||||
return '"' + s.replace('"', '\\"') + '"'
|
||||
|
||||
|
||||
def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
|
||||
return subprocess.Popen(
|
||||
args=args, cwd=cwd, stdout=stdout, stderr=stderr,
|
||||
shell=shell, close_fds=True, env=env,
|
||||
preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL))
|
||||
|
||||
|
||||
def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
|
||||
pipe = Popen(args, stdout=stdout, stderr=stderr, shell=shell, cwd=cwd,
|
||||
env=env)
|
||||
pipe.communicate()
|
||||
return pipe.wait()
|
||||
|
||||
|
||||
def RunCmd(args, cwd=None):
|
||||
"""Opens a subprocess to execute a program and returns its return value.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
|
||||
Returns:
|
||||
Return code from the command execution.
|
||||
"""
|
||||
logging.info(str(args) + ' ' + (cwd or ''))
|
||||
return Call(args, cwd=cwd)
|
||||
|
||||
|
||||
def GetCmdOutput(args, cwd=None, shell=False):
|
||||
"""Open a subprocess to execute a program and returns its output.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command.
|
||||
|
||||
Returns:
|
||||
Captures and returns the command's stdout.
|
||||
Prints the command's stderr to logger (which defaults to stdout).
|
||||
"""
|
||||
(_, output) = GetCmdStatusAndOutput(args, cwd, shell)
|
||||
return output
|
||||
|
||||
|
||||
def _ValidateAndLogCommand(args, cwd, shell):
|
||||
if isinstance(args, basestring):
|
||||
if not shell:
|
||||
raise Exception('string args must be run with shell=True')
|
||||
else:
|
||||
if shell:
|
||||
raise Exception('array args must be run with shell=False')
|
||||
args = ' '.join(SingleQuote(c) for c in args)
|
||||
if cwd is None:
|
||||
cwd = ''
|
||||
else:
|
||||
cwd = ':' + cwd
|
||||
logging.info('[host]%s> %s', cwd, args)
|
||||
return args
|
||||
|
||||
|
||||
def GetCmdStatusAndOutput(args, cwd=None, shell=False):
|
||||
"""Executes a subprocess and returns its exit code and output.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
|
||||
Returns:
|
||||
The 2-tuple (exit code, output).
|
||||
"""
|
||||
status, stdout, stderr = GetCmdStatusOutputAndError(
|
||||
args, cwd=cwd, shell=shell)
|
||||
|
||||
if stderr:
|
||||
logging.critical(stderr)
|
||||
if len(stdout) > 4096:
|
||||
logging.debug('Truncated output:')
|
||||
logging.debug(stdout[:4096])
|
||||
return (status, stdout)
|
||||
|
||||
def GetCmdStatusOutputAndError(args, cwd=None, shell=False):
|
||||
"""Executes a subprocess and returns its exit code, output, and errors.
|
||||
|
||||
Args:
|
||||
args: A string or a sequence of program arguments. The program to execute is
|
||||
the string or the first item in the args sequence.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
|
||||
Returns:
|
||||
The 2-tuple (exit code, output).
|
||||
"""
|
||||
_ValidateAndLogCommand(args, cwd, shell)
|
||||
pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
shell=shell, cwd=cwd)
|
||||
stdout, stderr = pipe.communicate()
|
||||
return (pipe.returncode, stdout, stderr)
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
"""Module-specific timeout exception."""
|
||||
|
||||
def __init__(self, output=None):
|
||||
super(TimeoutError, self).__init__()
|
||||
self._output = output
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
return self._output
|
||||
|
||||
|
||||
def _IterProcessStdout(process, timeout=None, buffer_size=4096,
|
||||
poll_interval=1):
|
||||
assert fcntl, 'fcntl module is required'
|
||||
try:
|
||||
# Enable non-blocking reads from the child's stdout.
|
||||
child_fd = process.stdout.fileno()
|
||||
fl = fcntl.fcntl(child_fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(child_fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
||||
|
||||
end_time = (time.time() + timeout) if timeout else None
|
||||
while True:
|
||||
if end_time and time.time() > end_time:
|
||||
raise TimeoutError()
|
||||
read_fds, _, _ = select.select([child_fd], [], [], poll_interval)
|
||||
if child_fd in read_fds:
|
||||
data = os.read(child_fd, buffer_size)
|
||||
if not data:
|
||||
break
|
||||
yield data
|
||||
if process.poll() is not None:
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
# Make sure the process doesn't stick around if we fail with an
|
||||
# exception.
|
||||
process.kill()
|
||||
except OSError:
|
||||
pass
|
||||
process.wait()
|
||||
|
||||
|
||||
def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
|
||||
logfile=None):
|
||||
"""Executes a subprocess with a timeout.
|
||||
|
||||
Args:
|
||||
args: List of arguments to the program, the program to execute is the first
|
||||
element.
|
||||
timeout: the timeout in seconds or None to wait forever.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
logfile: Optional file-like object that will receive output from the
|
||||
command as it is running.
|
||||
|
||||
Returns:
|
||||
The 2-tuple (exit code, output).
|
||||
"""
|
||||
_ValidateAndLogCommand(args, cwd, shell)
|
||||
output = StringIO.StringIO()
|
||||
process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
try:
|
||||
for data in _IterProcessStdout(process, timeout=timeout):
|
||||
if logfile:
|
||||
logfile.write(data)
|
||||
output.write(data)
|
||||
except TimeoutError:
|
||||
raise TimeoutError(output.getvalue())
|
||||
|
||||
return process.returncode, output.getvalue()
|
||||
|
||||
|
||||
def IterCmdOutputLines(args, timeout=None, cwd=None, shell=False,
|
||||
check_status=True):
|
||||
"""Executes a subprocess and continuously yields lines from its output.
|
||||
|
||||
Args:
|
||||
args: List of arguments to the program, the program to execute is the first
|
||||
element.
|
||||
cwd: If not None, the subprocess's current directory will be changed to
|
||||
|cwd| before it's executed.
|
||||
shell: Whether to execute args as a shell command. Must be True if args
|
||||
is a string and False if args is a sequence.
|
||||
check_status: A boolean indicating whether to check the exit status of the
|
||||
process after all output has been read.
|
||||
|
||||
Yields:
|
||||
The output of the subprocess, line by line.
|
||||
|
||||
Raises:
|
||||
CalledProcessError if check_status is True and the process exited with a
|
||||
non-zero exit status.
|
||||
"""
|
||||
cmd = _ValidateAndLogCommand(args, cwd, shell)
|
||||
process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
buffer_output = ''
|
||||
for data in _IterProcessStdout(process, timeout=timeout):
|
||||
buffer_output += data
|
||||
has_incomplete_line = buffer_output[-1] not in '\r\n'
|
||||
lines = buffer_output.splitlines()
|
||||
buffer_output = lines.pop() if has_incomplete_line else ''
|
||||
for line in lines:
|
||||
yield line
|
||||
if buffer_output:
|
||||
yield buffer_output
|
||||
if check_status and process.returncode:
|
||||
raise subprocess.CalledProcessError(process.returncode, cmd)
|
||||
from devil.utils.cmd_helper import *
|
||||
|
|
|
@ -13,6 +13,9 @@ import logging
|
|||
import os
|
||||
import subprocess
|
||||
|
||||
import devil.android.sdk.keyevent
|
||||
keyevent = devil.android.sdk.keyevent
|
||||
|
||||
|
||||
DIR_SOURCE_ROOT = os.environ.get('CHECKOUT_SOURCE_ROOT',
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
|
|
|
@ -1,649 +1,8 @@
|
|||
# Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""This module wraps Android's adb tool.
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
This is a thin wrapper around the adb interface. Any additional complexity
|
||||
should be delegated to a higher level (ex. DeviceUtils).
|
||||
"""
|
||||
|
||||
import collections
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.device import decorators
|
||||
from pylib.device import device_errors
|
||||
from pylib.utils import timeout_retry
|
||||
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 2
|
||||
|
||||
_EMULATOR_RE = re.compile(r'^emulator-[0-9]+$')
|
||||
|
||||
_READY_STATE = 'device'
|
||||
|
||||
|
||||
def _VerifyLocalFileExists(path):
|
||||
"""Verifies a local file exists.
|
||||
|
||||
Args:
|
||||
path: Path to the local file.
|
||||
|
||||
Raises:
|
||||
IOError: If the file doesn't exist.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
|
||||
DeviceStat = collections.namedtuple('DeviceStat',
|
||||
['st_mode', 'st_size', 'st_time'])
|
||||
|
||||
|
||||
class AdbWrapper(object):
|
||||
"""A wrapper around a local Android Debug Bridge executable."""
|
||||
|
||||
def __init__(self, device_serial):
|
||||
"""Initializes the AdbWrapper.
|
||||
|
||||
Args:
|
||||
device_serial: The device serial number as a string.
|
||||
"""
|
||||
if not device_serial:
|
||||
raise ValueError('A device serial must be specified')
|
||||
self._device_serial = str(device_serial)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@classmethod
|
||||
def _BuildAdbCmd(cls, args, device_serial, cpu_affinity=None):
|
||||
if cpu_affinity is not None:
|
||||
cmd = ['taskset', '-c', str(cpu_affinity)]
|
||||
else:
|
||||
cmd = []
|
||||
cmd.append(constants.GetAdbPath())
|
||||
if device_serial is not None:
|
||||
cmd.extend(['-s', device_serial])
|
||||
cmd.extend(args)
|
||||
return cmd
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@classmethod
|
||||
@decorators.WithTimeoutAndRetries
|
||||
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None,
|
||||
check_error=True, cpu_affinity=None):
|
||||
status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
|
||||
cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity),
|
||||
timeout_retry.CurrentTimeoutThread().GetRemainingTime())
|
||||
if status != 0:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
args, output, status, device_serial)
|
||||
# This catches some errors, including when the device drops offline;
|
||||
# unfortunately adb is very inconsistent with error reporting so many
|
||||
# command failures present differently.
|
||||
if check_error and output.startswith('error:'):
|
||||
raise device_errors.AdbCommandFailedError(args, output)
|
||||
return output
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
def _RunDeviceAdbCmd(self, args, timeout, retries, check_error=True):
|
||||
"""Runs an adb command on the device associated with this object.
|
||||
|
||||
Args:
|
||||
args: A list of arguments to adb.
|
||||
timeout: Timeout in seconds.
|
||||
retries: Number of retries.
|
||||
check_error: Check that the command doesn't return an error message. This
|
||||
does NOT check the exit status of shell commands.
|
||||
|
||||
Returns:
|
||||
The output of the command.
|
||||
"""
|
||||
return self._RunAdbCmd(args, timeout=timeout, retries=retries,
|
||||
device_serial=self._device_serial,
|
||||
check_error=check_error)
|
||||
|
||||
def _IterRunDeviceAdbCmd(self, args, timeout):
|
||||
"""Runs an adb command and returns an iterator over its output lines.
|
||||
|
||||
Args:
|
||||
args: A list of arguments to adb.
|
||||
timeout: Timeout in seconds.
|
||||
|
||||
Yields:
|
||||
The output of the command line by line.
|
||||
"""
|
||||
return cmd_helper.IterCmdOutputLines(
|
||||
self._BuildAdbCmd(args, self._device_serial), timeout=timeout)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Consider instances equal if they refer to the same device.
|
||||
|
||||
Args:
|
||||
other: The instance to compare equality with.
|
||||
|
||||
Returns:
|
||||
True if the instances are considered equal, false otherwise.
|
||||
"""
|
||||
return self._device_serial == str(other)
|
||||
|
||||
def __str__(self):
|
||||
"""The string representation of an instance.
|
||||
|
||||
Returns:
|
||||
The device serial number as a string.
|
||||
"""
|
||||
return self._device_serial
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(\'%s\')' % (self.__class__.__name__, self)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@classmethod
|
||||
def IsServerOnline(cls):
|
||||
status, output = cmd_helper.GetCmdStatusAndOutput(['pgrep', 'adb'])
|
||||
output = [int(x) for x in output.split()]
|
||||
logging.info('PIDs for adb found: %r', output)
|
||||
return status == 0
|
||||
# pylint: enable=unused-argument
|
||||
|
||||
@classmethod
|
||||
def KillServer(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
cls._RunAdbCmd(['kill-server'], timeout=timeout, retries=retries)
|
||||
|
||||
@classmethod
|
||||
def StartServer(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
# CPU affinity is used to reduce adb instability http://crbug.com/268450
|
||||
cls._RunAdbCmd(['start-server'], timeout=timeout, retries=retries,
|
||||
cpu_affinity=0)
|
||||
|
||||
@classmethod
|
||||
def GetDevices(cls, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""DEPRECATED. Refer to Devices(...) below."""
|
||||
# TODO(jbudorick): Remove this function once no more clients are using it.
|
||||
return cls.Devices(timeout=timeout, retries=retries)
|
||||
|
||||
@classmethod
|
||||
def Devices(cls, desired_state=_READY_STATE, long_list=False,
|
||||
timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Get the list of active attached devices.
|
||||
|
||||
Args:
|
||||
desired_state: If not None, limit the devices returned to only those
|
||||
in the given state.
|
||||
long_list: Whether to use the long listing format.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Yields:
|
||||
AdbWrapper instances.
|
||||
"""
|
||||
lines = cls._RawDevices(long_list=long_list, timeout=timeout,
|
||||
retries=retries)
|
||||
return [AdbWrapper(line[0]) for line in lines
|
||||
if ((long_list or len(line) == 2)
|
||||
and (not desired_state or line[1] == desired_state))]
|
||||
|
||||
@classmethod
|
||||
def _RawDevices(cls, long_list=False, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
cmd = ['devices']
|
||||
if long_list:
|
||||
cmd.append('-l')
|
||||
output = cls._RunAdbCmd(cmd, timeout=timeout, retries=retries)
|
||||
return [line.split() for line in output.splitlines()[1:]]
|
||||
|
||||
def GetDeviceSerial(self):
|
||||
"""Gets the device serial number associated with this object.
|
||||
|
||||
Returns:
|
||||
Device serial number as a string.
|
||||
"""
|
||||
return self._device_serial
|
||||
|
||||
def Push(self, local, remote, timeout=60*5, retries=_DEFAULT_RETRIES):
|
||||
"""Pushes a file from the host to the device.
|
||||
|
||||
Args:
|
||||
local: Path on the host filesystem.
|
||||
remote: Path on the device filesystem.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
_VerifyLocalFileExists(local)
|
||||
self._RunDeviceAdbCmd(['push', local, remote], timeout, retries)
|
||||
|
||||
def Pull(self, remote, local, timeout=60*5, retries=_DEFAULT_RETRIES):
|
||||
"""Pulls a file from the device to the host.
|
||||
|
||||
Args:
|
||||
remote: Path on the device filesystem.
|
||||
local: Path on the host filesystem.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
cmd = ['pull', remote, local]
|
||||
self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
try:
|
||||
_VerifyLocalFileExists(local)
|
||||
except IOError:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, 'File not found on host: %s' % local, device_serial=str(self))
|
||||
|
||||
def Shell(self, command, expect_status=0, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Runs a shell command on the device.
|
||||
|
||||
Args:
|
||||
command: A string with the shell command to run.
|
||||
expect_status: (optional) Check that the command's exit status matches
|
||||
this value. Default is 0. If set to None the test is skipped.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
The output of the shell command as a string.
|
||||
|
||||
Raises:
|
||||
device_errors.AdbCommandFailedError: If the exit status doesn't match
|
||||
|expect_status|.
|
||||
"""
|
||||
if expect_status is None:
|
||||
args = ['shell', command]
|
||||
else:
|
||||
args = ['shell', '(%s);echo %%$?' % command.rstrip()]
|
||||
output = self._RunDeviceAdbCmd(args, timeout, retries, check_error=False)
|
||||
if expect_status is not None:
|
||||
output_end = output.rfind('%')
|
||||
if output_end < 0:
|
||||
# causes the status string to become empty and raise a ValueError
|
||||
output_end = len(output)
|
||||
|
||||
try:
|
||||
status = int(output[output_end+1:])
|
||||
except ValueError:
|
||||
logging.warning('exit status of shell command %r missing.', command)
|
||||
raise device_errors.AdbShellCommandFailedError(
|
||||
command, output, status=None, device_serial=self._device_serial)
|
||||
output = output[:output_end]
|
||||
if status != expect_status:
|
||||
raise device_errors.AdbShellCommandFailedError(
|
||||
command, output, status=status, device_serial=self._device_serial)
|
||||
return output
|
||||
|
||||
def IterShell(self, command, timeout):
|
||||
"""Runs a shell command and returns an iterator over its output lines.
|
||||
|
||||
Args:
|
||||
command: A string with the shell command to run.
|
||||
timeout: Timeout in seconds.
|
||||
|
||||
Yields:
|
||||
The output of the command line by line.
|
||||
"""
|
||||
args = ['shell', command]
|
||||
return cmd_helper.IterCmdOutputLines(
|
||||
self._BuildAdbCmd(args, self._device_serial), timeout=timeout)
|
||||
|
||||
def Ls(self, path, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""List the contents of a directory on the device.
|
||||
|
||||
Args:
|
||||
path: Path on the device filesystem.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
A list of pairs (filename, stat) for each file found in the directory,
|
||||
where the stat object has the properties: st_mode, st_size, and st_time.
|
||||
|
||||
Raises:
|
||||
AdbCommandFailedError if |path| does not specify a valid and accessible
|
||||
directory in the device.
|
||||
"""
|
||||
def ParseLine(line):
|
||||
cols = line.split(None, 3)
|
||||
filename = cols.pop()
|
||||
stat = DeviceStat(*[int(num, base=16) for num in cols])
|
||||
return (filename, stat)
|
||||
|
||||
cmd = ['ls', path]
|
||||
lines = self._RunDeviceAdbCmd(
|
||||
cmd, timeout=timeout, retries=retries).splitlines()
|
||||
if lines:
|
||||
return [ParseLine(line) for line in lines]
|
||||
else:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, 'path does not specify an accessible directory in the device',
|
||||
device_serial=self._device_serial)
|
||||
|
||||
def Logcat(self, clear=False, dump=False, filter_specs=None,
|
||||
logcat_format=None, ring_buffer=None, timeout=None,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Get an iterable over the logcat output.
|
||||
|
||||
Args:
|
||||
clear: If true, clear the logcat.
|
||||
dump: If true, dump the current logcat contents.
|
||||
filter_specs: If set, a list of specs to filter the logcat.
|
||||
logcat_format: If set, the format in which the logcat should be output.
|
||||
Options include "brief", "process", "tag", "thread", "raw", "time",
|
||||
"threadtime", and "long"
|
||||
ring_buffer: If set, a list of alternate ring buffers to request.
|
||||
Options include "main", "system", "radio", "events", "crash" or "all".
|
||||
The default is equivalent to ["main", "system", "crash"].
|
||||
timeout: (optional) If set, timeout per try in seconds. If clear or dump
|
||||
is set, defaults to _DEFAULT_TIMEOUT.
|
||||
retries: (optional) If clear or dump is set, the number of retries to
|
||||
attempt. Otherwise, does nothing.
|
||||
|
||||
Yields:
|
||||
logcat output line by line.
|
||||
"""
|
||||
cmd = ['logcat']
|
||||
use_iter = True
|
||||
if clear:
|
||||
cmd.append('-c')
|
||||
use_iter = False
|
||||
if dump:
|
||||
cmd.append('-d')
|
||||
use_iter = False
|
||||
if logcat_format:
|
||||
cmd.extend(['-v', logcat_format])
|
||||
if ring_buffer:
|
||||
for buffer_name in ring_buffer:
|
||||
cmd.extend(['-b', buffer_name])
|
||||
if filter_specs:
|
||||
cmd.extend(filter_specs)
|
||||
|
||||
if use_iter:
|
||||
return self._IterRunDeviceAdbCmd(cmd, timeout)
|
||||
else:
|
||||
timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT
|
||||
return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines()
|
||||
|
||||
def Forward(self, local, remote, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Forward socket connections from the local socket to the remote socket.
|
||||
|
||||
Sockets are specified by one of:
|
||||
tcp:<port>
|
||||
localabstract:<unix domain socket name>
|
||||
localreserved:<unix domain socket name>
|
||||
localfilesystem:<unix domain socket name>
|
||||
dev:<character device name>
|
||||
jdwp:<process pid> (remote only)
|
||||
|
||||
Args:
|
||||
local: The host socket.
|
||||
remote: The device socket.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
self._RunDeviceAdbCmd(['forward', str(local), str(remote)], timeout,
|
||||
retries)
|
||||
|
||||
def ForwardRemove(self, local, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Remove a forward socket connection.
|
||||
|
||||
Args:
|
||||
local: The host socket.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
self._RunDeviceAdbCmd(['forward', '--remove', str(local)], timeout,
|
||||
retries)
|
||||
|
||||
def ForwardList(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""List all currently forwarded socket connections.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
return self._RunDeviceAdbCmd(['forward', '--list'], timeout, retries)
|
||||
|
||||
def JDWP(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""List of PIDs of processes hosting a JDWP transport.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
A list of PIDs as strings.
|
||||
"""
|
||||
return [a.strip() for a in
|
||||
self._RunDeviceAdbCmd(['jdwp'], timeout, retries).split('\n')]
|
||||
|
||||
def Install(self, apk_path, forward_lock=False, reinstall=False,
|
||||
sd_card=False, timeout=60*2, retries=_DEFAULT_RETRIES):
|
||||
"""Install an apk on the device.
|
||||
|
||||
Args:
|
||||
apk_path: Host path to the APK file.
|
||||
forward_lock: (optional) If set forward-locks the app.
|
||||
reinstall: (optional) If set reinstalls the app, keeping its data.
|
||||
sd_card: (optional) If set installs on the SD card.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
_VerifyLocalFileExists(apk_path)
|
||||
cmd = ['install']
|
||||
if forward_lock:
|
||||
cmd.append('-l')
|
||||
if reinstall:
|
||||
cmd.append('-r')
|
||||
if sd_card:
|
||||
cmd.append('-s')
|
||||
cmd.append(apk_path)
|
||||
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
if 'Success' not in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, output, device_serial=self._device_serial)
|
||||
|
||||
def InstallMultiple(self, apk_paths, forward_lock=False, reinstall=False,
|
||||
sd_card=False, allow_downgrade=False, partial=False,
|
||||
timeout=60*2, retries=_DEFAULT_RETRIES):
|
||||
"""Install an apk with splits on the device.
|
||||
|
||||
Args:
|
||||
apk_paths: Host path to the APK file.
|
||||
forward_lock: (optional) If set forward-locks the app.
|
||||
reinstall: (optional) If set reinstalls the app, keeping its data.
|
||||
sd_card: (optional) If set installs on the SD card.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
allow_downgrade: (optional) Allow versionCode downgrade.
|
||||
partial: (optional) Package ID if apk_paths doesn't include all .apks.
|
||||
"""
|
||||
for path in apk_paths:
|
||||
_VerifyLocalFileExists(path)
|
||||
cmd = ['install-multiple']
|
||||
if forward_lock:
|
||||
cmd.append('-l')
|
||||
if reinstall:
|
||||
cmd.append('-r')
|
||||
if sd_card:
|
||||
cmd.append('-s')
|
||||
if allow_downgrade:
|
||||
cmd.append('-d')
|
||||
if partial:
|
||||
cmd.extend(('-p', partial))
|
||||
cmd.extend(apk_paths)
|
||||
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
if 'Success' not in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, output, device_serial=self._device_serial)
|
||||
|
||||
def Uninstall(self, package, keep_data=False, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Remove the app |package| from the device.
|
||||
|
||||
Args:
|
||||
package: The package to uninstall.
|
||||
keep_data: (optional) If set keep the data and cache directories.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
cmd = ['uninstall']
|
||||
if keep_data:
|
||||
cmd.append('-k')
|
||||
cmd.append(package)
|
||||
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
if 'Failure' in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
cmd, output, device_serial=self._device_serial)
|
||||
|
||||
def Backup(self, path, packages=None, apk=False, shared=False,
|
||||
nosystem=True, include_all=False, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Write an archive of the device's data to |path|.
|
||||
|
||||
Args:
|
||||
path: Local path to store the backup file.
|
||||
packages: List of to packages to be backed up.
|
||||
apk: (optional) If set include the .apk files in the archive.
|
||||
shared: (optional) If set buckup the device's SD card.
|
||||
nosystem: (optional) If set exclude system applications.
|
||||
include_all: (optional) If set back up all installed applications and
|
||||
|packages| is optional.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
cmd = ['backup', '-f', path]
|
||||
if apk:
|
||||
cmd.append('-apk')
|
||||
if shared:
|
||||
cmd.append('-shared')
|
||||
if nosystem:
|
||||
cmd.append('-nosystem')
|
||||
if include_all:
|
||||
cmd.append('-all')
|
||||
if packages:
|
||||
cmd.extend(packages)
|
||||
assert bool(packages) ^ bool(include_all), (
|
||||
'Provide \'packages\' or set \'include_all\' but not both.')
|
||||
ret = self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
_VerifyLocalFileExists(path)
|
||||
return ret
|
||||
|
||||
def Restore(self, path, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Restore device contents from the backup archive.
|
||||
|
||||
Args:
|
||||
path: Host path to the backup archive.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
_VerifyLocalFileExists(path)
|
||||
self._RunDeviceAdbCmd(['restore'] + [path], timeout, retries)
|
||||
|
||||
def WaitForDevice(self, timeout=60*5, retries=_DEFAULT_RETRIES):
|
||||
"""Block until the device is online.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
self._RunDeviceAdbCmd(['wait-for-device'], timeout, retries)
|
||||
|
||||
def GetState(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Get device state.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
One of 'offline', 'bootloader', or 'device'.
|
||||
"""
|
||||
# TODO(jbudorick): Revert to using get-state once it doesn't cause a
|
||||
# a protocol fault.
|
||||
# return self._RunDeviceAdbCmd(['get-state'], timeout, retries).strip()
|
||||
|
||||
lines = self._RawDevices(timeout=timeout, retries=retries)
|
||||
for line in lines:
|
||||
if len(line) >= 2 and line[0] == self._device_serial:
|
||||
return line[1]
|
||||
return 'offline'
|
||||
|
||||
|
||||
def GetDevPath(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Gets the device path.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
The device path (e.g. usb:3-4)
|
||||
"""
|
||||
return self._RunDeviceAdbCmd(['get-devpath'], timeout, retries)
|
||||
|
||||
def Remount(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Remounts the /system partition on the device read-write."""
|
||||
self._RunDeviceAdbCmd(['remount'], timeout, retries)
|
||||
|
||||
def Reboot(self, to_bootloader=False, timeout=60*5,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Reboots the device.
|
||||
|
||||
Args:
|
||||
to_bootloader: (optional) If set reboots to the bootloader.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
if to_bootloader:
|
||||
cmd = ['reboot-bootloader']
|
||||
else:
|
||||
cmd = ['reboot']
|
||||
self._RunDeviceAdbCmd(cmd, timeout, retries)
|
||||
|
||||
def Root(self, timeout=_DEFAULT_TIMEOUT, retries=_DEFAULT_RETRIES):
|
||||
"""Restarts the adbd daemon with root permissions, if possible.
|
||||
|
||||
Args:
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
"""
|
||||
output = self._RunDeviceAdbCmd(['root'], timeout, retries)
|
||||
if 'cannot' in output:
|
||||
raise device_errors.AdbCommandFailedError(
|
||||
['root'], output, device_serial=self._device_serial)
|
||||
|
||||
def Emu(self, cmd, timeout=_DEFAULT_TIMEOUT,
|
||||
retries=_DEFAULT_RETRIES):
|
||||
"""Runs an emulator console command.
|
||||
|
||||
See http://developer.android.com/tools/devices/emulator.html#console
|
||||
|
||||
Args:
|
||||
cmd: The command to run on the emulator console.
|
||||
timeout: (optional) Timeout per try in seconds.
|
||||
retries: (optional) Number of retries to attempt.
|
||||
|
||||
Returns:
|
||||
The output of the emulator console command.
|
||||
"""
|
||||
if isinstance(cmd, basestring):
|
||||
cmd = [cmd]
|
||||
return self._RunDeviceAdbCmd(['emu'] + cmd, timeout, retries)
|
||||
|
||||
@property
|
||||
def is_emulator(self):
|
||||
return _EMULATOR_RE.match(self._device_serial)
|
||||
|
||||
@property
|
||||
def is_ready(self):
|
||||
try:
|
||||
return self.GetState() == _READY_STATE
|
||||
except device_errors.CommandFailedError:
|
||||
return False
|
||||
from devil.android.sdk.adb_wrapper import *
|
||||
|
|
|
@ -2,634 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Provides a variety of device interactions with power.
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import csv
|
||||
import logging
|
||||
|
||||
from pylib import constants
|
||||
from pylib.device import decorators
|
||||
from pylib.device import device_errors
|
||||
from pylib.device import device_utils
|
||||
from pylib.utils import timeout_retry
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 3
|
||||
|
||||
|
||||
_DEVICE_PROFILES = [
|
||||
{
|
||||
'name': 'Nexus 4',
|
||||
'witness_file': '/sys/module/pm8921_charger/parameters/disabled',
|
||||
'enable_command': (
|
||||
'echo 0 > /sys/module/pm8921_charger/parameters/disabled && '
|
||||
'dumpsys battery reset'),
|
||||
'disable_command': (
|
||||
'echo 1 > /sys/module/pm8921_charger/parameters/disabled && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': None,
|
||||
'voltage': None,
|
||||
'current': None,
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 5',
|
||||
# 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 && '
|
||||
'chmod 644 /sys/class/power_supply/usb/online && '
|
||||
'echo 1 > /sys/class/power_supply/usb/online && '
|
||||
'dumpsys battery reset'),
|
||||
'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 && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': None,
|
||||
'voltage': None,
|
||||
'current': None,
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 6',
|
||||
'witness_file': None,
|
||||
'enable_command': (
|
||||
'echo 1 > /sys/class/power_supply/battery/charging_enabled && '
|
||||
'dumpsys battery reset'),
|
||||
'disable_command': (
|
||||
'echo 0 > /sys/class/power_supply/battery/charging_enabled && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': (
|
||||
'/sys/class/power_supply/max170xx_battery/charge_counter_ext'),
|
||||
'voltage': '/sys/class/power_supply/max170xx_battery/voltage_now',
|
||||
'current': '/sys/class/power_supply/max170xx_battery/current_now',
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 9',
|
||||
'witness_file': None,
|
||||
'enable_command': (
|
||||
'echo Disconnected > '
|
||||
'/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
|
||||
'dumpsys battery reset'),
|
||||
'disable_command': (
|
||||
'echo Connected > '
|
||||
'/sys/bus/i2c/drivers/bq2419x/0-006b/input_cable_state && '
|
||||
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
|
||||
'charge_counter': '/sys/class/power_supply/battery/charge_counter_ext',
|
||||
'voltage': '/sys/class/power_supply/battery/voltage_now',
|
||||
'current': '/sys/class/power_supply/battery/current_now',
|
||||
},
|
||||
{
|
||||
'name': 'Nexus 10',
|
||||
'witness_file': None,
|
||||
'enable_command': None,
|
||||
'disable_command': None,
|
||||
'charge_counter': None,
|
||||
'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
|
||||
'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
# The list of useful dumpsys columns.
|
||||
# Index of the column containing the format version.
|
||||
_DUMP_VERSION_INDEX = 0
|
||||
# Index of the column containing the type of the row.
|
||||
_ROW_TYPE_INDEX = 3
|
||||
# Index of the column containing the uid.
|
||||
_PACKAGE_UID_INDEX = 4
|
||||
# Index of the column containing the application package.
|
||||
_PACKAGE_NAME_INDEX = 5
|
||||
# The column containing the uid of the power data.
|
||||
_PWI_UID_INDEX = 1
|
||||
# The column containing the type of consumption. Only consumption since last
|
||||
# charge are of interest here.
|
||||
_PWI_AGGREGATION_INDEX = 2
|
||||
_PWS_AGGREGATION_INDEX = _PWI_AGGREGATION_INDEX
|
||||
# The column containing the amount of power used, in mah.
|
||||
_PWI_POWER_CONSUMPTION_INDEX = 5
|
||||
_PWS_POWER_CONSUMPTION_INDEX = _PWI_POWER_CONSUMPTION_INDEX
|
||||
|
||||
|
||||
class BatteryUtils(object):
|
||||
|
||||
def __init__(self, device, default_timeout=_DEFAULT_TIMEOUT,
|
||||
default_retries=_DEFAULT_RETRIES):
|
||||
"""BatteryUtils constructor.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils instance.
|
||||
default_timeout: An integer containing the default number of seconds to
|
||||
wait for an operation to complete if no explicit value
|
||||
is provided.
|
||||
default_retries: An integer containing the default number or times an
|
||||
operation should be retried on failure if no explicit
|
||||
value is provided.
|
||||
|
||||
Raises:
|
||||
TypeError: If it is not passed a DeviceUtils instance.
|
||||
"""
|
||||
if not isinstance(device, device_utils.DeviceUtils):
|
||||
raise TypeError('Must be initialized with DeviceUtils object.')
|
||||
self._device = device
|
||||
self._cache = device.GetClientCache(self.__class__.__name__)
|
||||
self._default_timeout = default_timeout
|
||||
self._default_retries = default_retries
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def SupportsFuelGauge(self, timeout=None, retries=None):
|
||||
"""Detect if fuel gauge chip is present.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
True if known fuel gauge files are present.
|
||||
False otherwise.
|
||||
"""
|
||||
self._DiscoverDeviceProfile()
|
||||
return (self._cache['profile']['enable_command'] != None
|
||||
and self._cache['profile']['charge_counter'] != None)
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetFuelGaugeChargeCounter(self, timeout=None, retries=None):
|
||||
"""Get value of charge_counter on fuel gauge chip.
|
||||
|
||||
Device must have charging disabled for this, not just battery updates
|
||||
disabled. The only device that this currently works with is the nexus 5.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
value of charge_counter for fuel gauge chip in units of nAh.
|
||||
|
||||
Raises:
|
||||
device_errors.CommandFailedError: If fuel gauge chip not found.
|
||||
"""
|
||||
if self.SupportsFuelGauge():
|
||||
return int(self._device.ReadFile(
|
||||
self._cache['profile']['charge_counter']))
|
||||
raise device_errors.CommandFailedError(
|
||||
'Unable to find fuel gauge.')
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetNetworkData(self, package, timeout=None, retries=None):
|
||||
"""Get network data for specific package.
|
||||
|
||||
Args:
|
||||
package: package name you want network data for.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
Tuple of (sent_data, recieved_data)
|
||||
None if no network data found
|
||||
"""
|
||||
# If device_utils clears cache, cache['uids'] doesn't exist
|
||||
if 'uids' not in self._cache:
|
||||
self._cache['uids'] = {}
|
||||
if package not in self._cache['uids']:
|
||||
self.GetPowerData()
|
||||
if package not in self._cache['uids']:
|
||||
logging.warning('No UID found for %s. Can\'t get network data.',
|
||||
package)
|
||||
return None
|
||||
|
||||
network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
|
||||
try:
|
||||
send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
|
||||
# If ReadFile throws exception, it means no network data usage file for
|
||||
# package has been recorded. Return 0 sent and 0 received.
|
||||
except device_errors.AdbShellCommandFailedError:
|
||||
logging.warning('No sent data found for package %s', package)
|
||||
send_data = 0
|
||||
try:
|
||||
recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
|
||||
except device_errors.AdbShellCommandFailedError:
|
||||
logging.warning('No received data found for package %s', package)
|
||||
recv_data = 0
|
||||
return (send_data, recv_data)
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetPowerData(self, timeout=None, retries=None):
|
||||
"""Get power data for device.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
Dict containing system power, and a per-package power dict keyed on
|
||||
package names.
|
||||
{
|
||||
'system_total': 23.1,
|
||||
'per_package' : {
|
||||
package_name: {
|
||||
'uid': uid,
|
||||
'data': [1,2,3]
|
||||
},
|
||||
}
|
||||
}
|
||||
"""
|
||||
if 'uids' not in self._cache:
|
||||
self._cache['uids'] = {}
|
||||
dumpsys_output = self._device.RunShellCommand(
|
||||
['dumpsys', 'batterystats', '-c'],
|
||||
check_return=True, large_output=True)
|
||||
csvreader = csv.reader(dumpsys_output)
|
||||
pwi_entries = collections.defaultdict(list)
|
||||
system_total = None
|
||||
for entry in csvreader:
|
||||
if entry[_DUMP_VERSION_INDEX] not in ['8', '9']:
|
||||
# Wrong dumpsys version.
|
||||
raise device_errors.DeviceVersionError(
|
||||
'Dumpsys version must be 8 or 9. %s found.'
|
||||
% entry[_DUMP_VERSION_INDEX])
|
||||
if _ROW_TYPE_INDEX < len(entry) and entry[_ROW_TYPE_INDEX] == 'uid':
|
||||
current_package = entry[_PACKAGE_NAME_INDEX]
|
||||
if (self._cache['uids'].get(current_package)
|
||||
and self._cache['uids'].get(current_package)
|
||||
!= entry[_PACKAGE_UID_INDEX]):
|
||||
raise device_errors.CommandFailedError(
|
||||
'Package %s found multiple times with different UIDs %s and %s'
|
||||
% (current_package, self._cache['uids'][current_package],
|
||||
entry[_PACKAGE_UID_INDEX]))
|
||||
self._cache['uids'][current_package] = entry[_PACKAGE_UID_INDEX]
|
||||
elif (_PWI_POWER_CONSUMPTION_INDEX < len(entry)
|
||||
and entry[_ROW_TYPE_INDEX] == 'pwi'
|
||||
and entry[_PWI_AGGREGATION_INDEX] == 'l'):
|
||||
pwi_entries[entry[_PWI_UID_INDEX]].append(
|
||||
float(entry[_PWI_POWER_CONSUMPTION_INDEX]))
|
||||
elif (_PWS_POWER_CONSUMPTION_INDEX < len(entry)
|
||||
and entry[_ROW_TYPE_INDEX] == 'pws'
|
||||
and entry[_PWS_AGGREGATION_INDEX] == 'l'):
|
||||
# This entry should only appear once.
|
||||
assert system_total is None
|
||||
system_total = float(entry[_PWS_POWER_CONSUMPTION_INDEX])
|
||||
|
||||
per_package = {p: {'uid': uid, 'data': pwi_entries[uid]}
|
||||
for p, uid in self._cache['uids'].iteritems()}
|
||||
return {'system_total': system_total, 'per_package': per_package}
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetBatteryInfo(self, timeout=None, retries=None):
|
||||
"""Gets battery info for the device.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
Returns:
|
||||
A dict containing various battery information as reported by dumpsys
|
||||
battery.
|
||||
"""
|
||||
result = {}
|
||||
# Skip the first line, which is just a header.
|
||||
for line in self._device.RunShellCommand(
|
||||
['dumpsys', 'battery'], check_return=True)[1:]:
|
||||
# If usb charging has been disabled, an extra line of header exists.
|
||||
if 'UPDATES STOPPED' in line:
|
||||
logging.warning('Dumpsys battery not receiving updates. '
|
||||
'Run dumpsys battery reset if this is in error.')
|
||||
elif ':' not in line:
|
||||
logging.warning('Unknown line found in dumpsys battery: "%s"', line)
|
||||
else:
|
||||
k, v = line.split(':', 1)
|
||||
result[k.strip()] = v.strip()
|
||||
return result
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def GetCharging(self, timeout=None, retries=None):
|
||||
"""Gets the charging state of the device.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
Returns:
|
||||
True if the device is charging, false otherwise.
|
||||
"""
|
||||
battery_info = self.GetBatteryInfo()
|
||||
for k in ('AC powered', 'USB powered', 'Wireless powered'):
|
||||
if (k in battery_info and
|
||||
battery_info[k].lower() in ('true', '1', 'yes')):
|
||||
return True
|
||||
return False
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def SetCharging(self, enabled, timeout=None, retries=None):
|
||||
"""Enables or disables charging on the device.
|
||||
|
||||
Args:
|
||||
enabled: A boolean indicating whether charging should be enabled or
|
||||
disabled.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.CommandFailedError: If method of disabling charging cannot
|
||||
be determined.
|
||||
"""
|
||||
self._DiscoverDeviceProfile()
|
||||
if not self._cache['profile']['enable_command']:
|
||||
raise device_errors.CommandFailedError(
|
||||
'Unable to find charging commands.')
|
||||
|
||||
if enabled:
|
||||
command = self._cache['profile']['enable_command']
|
||||
else:
|
||||
command = self._cache['profile']['disable_command']
|
||||
|
||||
def verify_charging():
|
||||
return self.GetCharging() == enabled
|
||||
|
||||
self._device.RunShellCommand(
|
||||
command, check_return=True, as_root=True, large_output=True)
|
||||
timeout_retry.WaitFor(verify_charging, wait_period=1)
|
||||
|
||||
# TODO(rnephew): Make private when all use cases can use the context manager.
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def DisableBatteryUpdates(self, timeout=None, retries=None):
|
||||
"""Resets battery data and makes device appear like it is not
|
||||
charging so that it will collect power data since last charge.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.CommandFailedError: When resetting batterystats fails to
|
||||
reset power values.
|
||||
device_errors.DeviceVersionError: If device is not L or higher.
|
||||
"""
|
||||
def battery_updates_disabled():
|
||||
return self.GetCharging() is False
|
||||
|
||||
self._ClearPowerData()
|
||||
self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'ac', '0'],
|
||||
check_return=True)
|
||||
self._device.RunShellCommand(['dumpsys', 'battery', 'set', 'usb', '0'],
|
||||
check_return=True)
|
||||
timeout_retry.WaitFor(battery_updates_disabled, wait_period=1)
|
||||
|
||||
# TODO(rnephew): Make private when all use cases can use the context manager.
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def EnableBatteryUpdates(self, timeout=None, retries=None):
|
||||
"""Restarts device charging so that dumpsys no longer collects power data.
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.DeviceVersionError: If device is not L or higher.
|
||||
"""
|
||||
def battery_updates_enabled():
|
||||
return (self.GetCharging()
|
||||
or not bool('UPDATES STOPPED' in self._device.RunShellCommand(
|
||||
['dumpsys', 'battery'], check_return=True)))
|
||||
|
||||
self._device.RunShellCommand(['dumpsys', 'battery', 'reset'],
|
||||
check_return=True)
|
||||
timeout_retry.WaitFor(battery_updates_enabled, wait_period=1)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def BatteryMeasurement(self, timeout=None, retries=None):
|
||||
"""Context manager that enables battery data collection. It makes
|
||||
the device appear to stop charging so that dumpsys will start collecting
|
||||
power data since last charge. Once the with block is exited, charging is
|
||||
resumed and power data since last charge is no longer collected.
|
||||
|
||||
Only for devices L and higher.
|
||||
|
||||
Example usage:
|
||||
with BatteryMeasurement():
|
||||
browser_actions()
|
||||
get_power_data() # report usage within this block
|
||||
after_measurements() # Anything that runs after power
|
||||
# measurements are collected
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Raises:
|
||||
device_errors.DeviceVersionError: If device is not L or higher.
|
||||
"""
|
||||
if (self._device.build_version_sdk <
|
||||
constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
|
||||
raise device_errors.DeviceVersionError('Device must be L or higher.')
|
||||
try:
|
||||
self.DisableBatteryUpdates(timeout=timeout, retries=retries)
|
||||
yield
|
||||
finally:
|
||||
self.EnableBatteryUpdates(timeout=timeout, retries=retries)
|
||||
|
||||
def _DischargeDevice(self, percent, wait_period=120):
|
||||
"""Disables charging and waits for device to discharge given amount
|
||||
|
||||
Args:
|
||||
percent: level of charge to discharge.
|
||||
|
||||
Raises:
|
||||
ValueError: If percent is not between 1 and 99.
|
||||
"""
|
||||
battery_level = int(self.GetBatteryInfo().get('level'))
|
||||
if not 0 < percent < 100:
|
||||
raise ValueError('Discharge amount(%s) must be between 1 and 99'
|
||||
% percent)
|
||||
if battery_level is None:
|
||||
logging.warning('Unable to find current battery level. Cannot discharge.')
|
||||
return
|
||||
# Do not discharge if it would make battery level too low.
|
||||
if percent >= battery_level - 10:
|
||||
logging.warning('Battery is too low or discharge amount requested is too '
|
||||
'high. Cannot discharge phone %s percent.', percent)
|
||||
return
|
||||
|
||||
self.SetCharging(False)
|
||||
def device_discharged():
|
||||
self.SetCharging(True)
|
||||
current_level = int(self.GetBatteryInfo().get('level'))
|
||||
logging.info('current battery level: %s', current_level)
|
||||
if battery_level - current_level >= percent:
|
||||
return True
|
||||
self.SetCharging(False)
|
||||
return False
|
||||
|
||||
timeout_retry.WaitFor(device_discharged, wait_period=wait_period)
|
||||
|
||||
def ChargeDeviceToLevel(self, level, wait_period=60):
|
||||
"""Enables charging and waits for device to be charged to given level.
|
||||
|
||||
Args:
|
||||
level: level of charge to wait for.
|
||||
wait_period: time in seconds to wait between checking.
|
||||
"""
|
||||
self.SetCharging(True)
|
||||
|
||||
def device_charged():
|
||||
battery_level = self.GetBatteryInfo().get('level')
|
||||
if battery_level is None:
|
||||
logging.warning('Unable to find current battery level.')
|
||||
battery_level = 100
|
||||
else:
|
||||
logging.info('current battery level: %s', battery_level)
|
||||
battery_level = int(battery_level)
|
||||
return battery_level >= level
|
||||
|
||||
timeout_retry.WaitFor(device_charged, wait_period=wait_period)
|
||||
|
||||
def LetBatteryCoolToTemperature(self, target_temp, wait_period=180):
|
||||
"""Lets device sit to give battery time to cool down
|
||||
Args:
|
||||
temp: maximum temperature to allow in tenths of degrees c.
|
||||
wait_period: time in seconds to wait between checking.
|
||||
"""
|
||||
def cool_device():
|
||||
temp = self.GetBatteryInfo().get('temperature')
|
||||
if temp is None:
|
||||
logging.warning('Unable to find current battery temperature.')
|
||||
temp = 0
|
||||
else:
|
||||
logging.info('Current battery temperature: %s', temp)
|
||||
if int(temp) <= target_temp:
|
||||
return True
|
||||
else:
|
||||
if self._cache['profile']['name'] == 'Nexus 5':
|
||||
self._DischargeDevice(1)
|
||||
return False
|
||||
|
||||
self._DiscoverDeviceProfile()
|
||||
self.EnableBatteryUpdates()
|
||||
logging.info('Waiting for the device to cool down to %s (0.1 C)',
|
||||
target_temp)
|
||||
timeout_retry.WaitFor(cool_device, wait_period=wait_period)
|
||||
|
||||
@decorators.WithTimeoutAndRetriesFromInstance()
|
||||
def TieredSetCharging(self, enabled, timeout=None, retries=None):
|
||||
"""Enables or disables charging on the device.
|
||||
|
||||
Args:
|
||||
enabled: A boolean indicating whether charging should be enabled or
|
||||
disabled.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
"""
|
||||
if self.GetCharging() == enabled:
|
||||
logging.warning('Device charging already in expected state: %s', enabled)
|
||||
return
|
||||
|
||||
self._DiscoverDeviceProfile()
|
||||
if enabled:
|
||||
if self._cache['profile']['enable_command']:
|
||||
self.SetCharging(enabled)
|
||||
else:
|
||||
logging.info('Unable to enable charging via hardware. '
|
||||
'Falling back to software enabling.')
|
||||
self.EnableBatteryUpdates()
|
||||
else:
|
||||
if self._cache['profile']['enable_command']:
|
||||
self._ClearPowerData()
|
||||
self.SetCharging(enabled)
|
||||
else:
|
||||
logging.info('Unable to disable charging via hardware. '
|
||||
'Falling back to software disabling.')
|
||||
self.DisableBatteryUpdates()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def PowerMeasurement(self, timeout=None, retries=None):
|
||||
"""Context manager that enables battery power collection.
|
||||
|
||||
Once the with block is exited, charging is resumed. Will attempt to disable
|
||||
charging at the hardware level, and if that fails will fall back to software
|
||||
disabling of battery updates.
|
||||
|
||||
Only for devices L and higher.
|
||||
|
||||
Example usage:
|
||||
with PowerMeasurement():
|
||||
browser_actions()
|
||||
get_power_data() # report usage within this block
|
||||
after_measurements() # Anything that runs after power
|
||||
# measurements are collected
|
||||
|
||||
Args:
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
"""
|
||||
try:
|
||||
self.TieredSetCharging(False, timeout=timeout, retries=retries)
|
||||
yield
|
||||
finally:
|
||||
self.TieredSetCharging(True, timeout=timeout, retries=retries)
|
||||
|
||||
def _ClearPowerData(self):
|
||||
"""Resets battery data and makes device appear like it is not
|
||||
charging so that it will collect power data since last charge.
|
||||
|
||||
Returns:
|
||||
True if power data cleared.
|
||||
False if power data clearing is not supported (pre-L)
|
||||
|
||||
Raises:
|
||||
device_errors.DeviceVersionError: If power clearing is supported,
|
||||
but fails.
|
||||
"""
|
||||
if (self._device.build_version_sdk <
|
||||
constants.ANDROID_SDK_VERSION_CODES.LOLLIPOP):
|
||||
logging.warning('Dumpsys power data only available on 5.0 and above. '
|
||||
'Cannot clear power data.')
|
||||
return False
|
||||
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True)
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True)
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'batterystats', '--reset'], check_return=True)
|
||||
battery_data = self._device.RunShellCommand(
|
||||
['dumpsys', 'batterystats', '--charged', '-c'],
|
||||
check_return=True, large_output=True)
|
||||
for line in battery_data:
|
||||
l = line.split(',')
|
||||
if (len(l) > _PWI_POWER_CONSUMPTION_INDEX and l[_ROW_TYPE_INDEX] == 'pwi'
|
||||
and l[_PWI_POWER_CONSUMPTION_INDEX] != 0):
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'reset'], check_return=True)
|
||||
raise device_errors.CommandFailedError(
|
||||
'Non-zero pmi value found after reset.')
|
||||
self._device.RunShellCommand(
|
||||
['dumpsys', 'battery', 'reset'], check_return=True)
|
||||
return True
|
||||
|
||||
def _DiscoverDeviceProfile(self):
|
||||
"""Checks and caches device information.
|
||||
|
||||
Returns:
|
||||
True if profile is found, false otherwise.
|
||||
"""
|
||||
|
||||
if 'profile' in self._cache:
|
||||
return True
|
||||
for profile in _DEVICE_PROFILES:
|
||||
if self._device.product_model == profile['name']:
|
||||
self._cache['profile'] = profile
|
||||
return True
|
||||
self._cache['profile'] = {
|
||||
'name': None,
|
||||
'witness_file': None,
|
||||
'enable_command': None,
|
||||
'disable_command': None,
|
||||
'charge_counter': None,
|
||||
'voltage': None,
|
||||
'current': None,
|
||||
}
|
||||
return False
|
||||
from devil.android.battery_utils import *
|
||||
|
|
|
@ -1,145 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""
|
||||
Function/method decorators that provide timeout and retry logic.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.device import device_errors
|
||||
from pylib.utils import reraiser_thread
|
||||
from pylib.utils import timeout_retry
|
||||
|
||||
DEFAULT_TIMEOUT_ATTR = '_default_timeout'
|
||||
DEFAULT_RETRIES_ATTR = '_default_retries'
|
||||
|
||||
|
||||
def _TimeoutRetryWrapper(f, timeout_func, retries_func, pass_values=False):
|
||||
""" Wraps a funcion with timeout and retry handling logic.
|
||||
|
||||
Args:
|
||||
f: The function to wrap.
|
||||
timeout_func: A callable that returns the timeout value.
|
||||
retries_func: A callable that returns the retries value.
|
||||
pass_values: If True, passes the values returned by |timeout_func| and
|
||||
|retries_func| to the wrapped function as 'timeout' and
|
||||
'retries' kwargs, respectively.
|
||||
Returns:
|
||||
The wrapped function.
|
||||
"""
|
||||
@functools.wraps(f)
|
||||
def TimeoutRetryWrapper(*args, **kwargs):
|
||||
timeout = timeout_func(*args, **kwargs)
|
||||
retries = retries_func(*args, **kwargs)
|
||||
if pass_values:
|
||||
kwargs['timeout'] = timeout
|
||||
kwargs['retries'] = retries
|
||||
def impl():
|
||||
return f(*args, **kwargs)
|
||||
try:
|
||||
if isinstance(threading.current_thread(),
|
||||
timeout_retry.TimeoutRetryThread):
|
||||
return impl()
|
||||
else:
|
||||
return timeout_retry.Run(impl, timeout, retries)
|
||||
except reraiser_thread.TimeoutError as e:
|
||||
raise device_errors.CommandTimeoutError(str(e)), None, (
|
||||
sys.exc_info()[2])
|
||||
except cmd_helper.TimeoutError as e:
|
||||
raise device_errors.CommandTimeoutError(str(e)), None, (
|
||||
sys.exc_info()[2])
|
||||
return TimeoutRetryWrapper
|
||||
|
||||
|
||||
def WithTimeoutAndRetries(f):
|
||||
"""A decorator that handles timeouts and retries.
|
||||
|
||||
'timeout' and 'retries' kwargs must be passed to the function.
|
||||
|
||||
Args:
|
||||
f: The function to decorate.
|
||||
Returns:
|
||||
The decorated function.
|
||||
"""
|
||||
get_timeout = lambda *a, **kw: kw['timeout']
|
||||
get_retries = lambda *a, **kw: kw['retries']
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries)
|
||||
|
||||
|
||||
def WithExplicitTimeoutAndRetries(timeout, retries):
|
||||
"""Returns a decorator that handles timeouts and retries.
|
||||
|
||||
The provided |timeout| and |retries| values are always used.
|
||||
|
||||
Args:
|
||||
timeout: The number of seconds to wait for the decorated function to
|
||||
return. Always used.
|
||||
retries: The number of times the decorated function should be retried on
|
||||
failure. Always used.
|
||||
Returns:
|
||||
The actual decorator.
|
||||
"""
|
||||
def decorator(f):
|
||||
get_timeout = lambda *a, **kw: timeout
|
||||
get_retries = lambda *a, **kw: retries
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries)
|
||||
return decorator
|
||||
|
||||
|
||||
def WithTimeoutAndRetriesDefaults(default_timeout, default_retries):
|
||||
"""Returns a decorator that handles timeouts and retries.
|
||||
|
||||
The provided |default_timeout| and |default_retries| values are used only
|
||||
if timeout and retries values are not provided.
|
||||
|
||||
Args:
|
||||
default_timeout: The number of seconds to wait for the decorated function
|
||||
to return. Only used if a 'timeout' kwarg is not passed
|
||||
to the decorated function.
|
||||
default_retries: The number of times the decorated function should be
|
||||
retried on failure. Only used if a 'retries' kwarg is not
|
||||
passed to the decorated function.
|
||||
Returns:
|
||||
The actual decorator.
|
||||
"""
|
||||
def decorator(f):
|
||||
get_timeout = lambda *a, **kw: kw.get('timeout', default_timeout)
|
||||
get_retries = lambda *a, **kw: kw.get('retries', default_retries)
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries, pass_values=True)
|
||||
return decorator
|
||||
|
||||
|
||||
def WithTimeoutAndRetriesFromInstance(
|
||||
default_timeout_name=DEFAULT_TIMEOUT_ATTR,
|
||||
default_retries_name=DEFAULT_RETRIES_ATTR):
|
||||
"""Returns a decorator that handles timeouts and retries.
|
||||
|
||||
The provided |default_timeout_name| and |default_retries_name| are used to
|
||||
get the default timeout value and the default retries value from the object
|
||||
instance if timeout and retries values are not provided.
|
||||
|
||||
Note that this should only be used to decorate methods, not functions.
|
||||
|
||||
Args:
|
||||
default_timeout_name: The name of the default timeout attribute of the
|
||||
instance.
|
||||
default_retries_name: The name of the default retries attribute of the
|
||||
instance.
|
||||
Returns:
|
||||
The actual decorator.
|
||||
"""
|
||||
def decorator(f):
|
||||
def get_timeout(inst, *_args, **kwargs):
|
||||
return kwargs.get('timeout', getattr(inst, default_timeout_name))
|
||||
def get_retries(inst, *_args, **kwargs):
|
||||
return kwargs.get('retries', getattr(inst, default_retries_name))
|
||||
return _TimeoutRetryWrapper(f, get_timeout, get_retries, pass_values=True)
|
||||
return decorator
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.android.decorators import *
|
||||
|
|
|
@ -1,82 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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 json
|
||||
import os
|
||||
import threading
|
||||
|
||||
from pylib import constants
|
||||
|
||||
# TODO(jbudorick): Remove this once the blacklist is optional.
|
||||
BLACKLIST_JSON = os.path.join(
|
||||
constants.DIR_SOURCE_ROOT,
|
||||
os.environ.get('CHROMIUM_OUT_DIR', 'out'),
|
||||
'bad_devices.json')
|
||||
|
||||
class Blacklist(object):
|
||||
|
||||
def __init__(self, path):
|
||||
self._blacklist_lock = threading.RLock()
|
||||
self._path = path
|
||||
|
||||
def Read(self):
|
||||
"""Reads the blacklist from the blacklist file.
|
||||
|
||||
Returns:
|
||||
A list containing bad devices.
|
||||
"""
|
||||
with self._blacklist_lock:
|
||||
if not os.path.exists(self._path):
|
||||
return []
|
||||
|
||||
with open(self._path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def Write(self, blacklist):
|
||||
"""Writes the provided blacklist to the blacklist file.
|
||||
|
||||
Args:
|
||||
blacklist: list of bad devices to write to the blacklist file.
|
||||
"""
|
||||
with self._blacklist_lock:
|
||||
with open(self._path, 'w') as f:
|
||||
json.dump(list(set(blacklist)), f)
|
||||
|
||||
def Extend(self, devices):
|
||||
"""Adds devices to blacklist file.
|
||||
|
||||
Args:
|
||||
devices: list of bad devices to be added to the blacklist file.
|
||||
"""
|
||||
with self._blacklist_lock:
|
||||
blacklist = ReadBlacklist()
|
||||
blacklist.extend(devices)
|
||||
WriteBlacklist(blacklist)
|
||||
|
||||
def Reset(self):
|
||||
"""Erases the blacklist file if it exists."""
|
||||
with self._blacklist_lock:
|
||||
if os.path.exists(self._path):
|
||||
os.remove(self._path)
|
||||
|
||||
|
||||
def ReadBlacklist():
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
return Blacklist(BLACKLIST_JSON).Read()
|
||||
|
||||
|
||||
def WriteBlacklist(blacklist):
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
Blacklist(BLACKLIST_JSON).Write(blacklist)
|
||||
|
||||
|
||||
def ExtendBlacklist(devices):
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
Blacklist(BLACKLIST_JSON).Extend(devices)
|
||||
|
||||
|
||||
def ResetBlacklist():
|
||||
# TODO(jbudorick): Phase out once all clients have migrated.
|
||||
Blacklist(BLACKLIST_JSON).Reset()
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.android.device_blacklist import *
|
||||
|
|
|
@ -1,89 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""
|
||||
Exception classes raised by AdbWrapper and DeviceUtils.
|
||||
"""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib.utils import base_error
|
||||
|
||||
|
||||
class CommandFailedError(base_error.BaseError):
|
||||
"""Exception for command failures."""
|
||||
|
||||
def __init__(self, message, device_serial=None):
|
||||
if device_serial is not None:
|
||||
message = '(device: %s) %s' % (device_serial, message)
|
||||
self.device_serial = device_serial
|
||||
super(CommandFailedError, self).__init__(message)
|
||||
|
||||
|
||||
class AdbCommandFailedError(CommandFailedError):
|
||||
"""Exception for adb command failures."""
|
||||
|
||||
def __init__(self, args, output, status=None, device_serial=None,
|
||||
message=None):
|
||||
self.args = args
|
||||
self.output = output
|
||||
self.status = status
|
||||
if not message:
|
||||
adb_cmd = ' '.join(cmd_helper.SingleQuote(arg) for arg in self.args)
|
||||
message = ['adb %s: failed ' % adb_cmd]
|
||||
if status:
|
||||
message.append('with exit status %s ' % self.status)
|
||||
if output:
|
||||
message.append('and output:\n')
|
||||
message.extend('- %s\n' % line for line in output.splitlines())
|
||||
else:
|
||||
message.append('and no output.')
|
||||
message = ''.join(message)
|
||||
super(AdbCommandFailedError, self).__init__(message, device_serial)
|
||||
|
||||
|
||||
class DeviceVersionError(CommandFailedError):
|
||||
"""Exception for device version failures."""
|
||||
|
||||
def __init__(self, message, device_serial=None):
|
||||
super(DeviceVersionError, self).__init__(message, device_serial)
|
||||
|
||||
|
||||
class AdbShellCommandFailedError(AdbCommandFailedError):
|
||||
"""Exception for shell command failures run via adb."""
|
||||
|
||||
def __init__(self, command, output, status, device_serial=None):
|
||||
self.command = command
|
||||
message = ['shell command run via adb failed on the device:\n',
|
||||
' command: %s\n' % command]
|
||||
message.append(' exit status: %s\n' % status)
|
||||
if output:
|
||||
message.append(' output:\n')
|
||||
if isinstance(output, basestring):
|
||||
output_lines = output.splitlines()
|
||||
else:
|
||||
output_lines = output
|
||||
message.extend(' - %s\n' % line for line in output_lines)
|
||||
else:
|
||||
message.append(" output: ''\n")
|
||||
message = ''.join(message)
|
||||
super(AdbShellCommandFailedError, self).__init__(
|
||||
['shell', command], output, status, device_serial, message)
|
||||
|
||||
|
||||
class CommandTimeoutError(base_error.BaseError):
|
||||
"""Exception for command timeouts."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceUnreachableError(base_error.BaseError):
|
||||
"""Exception for device unreachable failures."""
|
||||
pass
|
||||
|
||||
|
||||
class NoDevicesError(base_error.BaseError):
|
||||
"""Exception for having no devices attached."""
|
||||
|
||||
def __init__(self):
|
||||
super(NoDevicesError, self).__init__(
|
||||
'No devices attached.', is_infra_error=True)
|
||||
from devil.android.device_errors import *
|
||||
|
|
|
@ -1,30 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""A module to keep track of devices across builds."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import os
|
||||
|
||||
LAST_DEVICES_FILENAME = '.last_devices'
|
||||
LAST_MISSING_DEVICES_FILENAME = '.last_missing'
|
||||
|
||||
|
||||
def GetPersistentDeviceList(file_name):
|
||||
"""Returns a list of devices.
|
||||
|
||||
Args:
|
||||
file_name: the file name containing a list of devices.
|
||||
|
||||
Returns: List of device serial numbers that were on the bot.
|
||||
"""
|
||||
with open(file_name) as f:
|
||||
return f.read().splitlines()
|
||||
|
||||
|
||||
def WritePersistentDeviceList(file_name, device_list):
|
||||
path = os.path.dirname(file_name)
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
with open(file_name, 'w') as f:
|
||||
f.write('\n'.join(set(device_list)))
|
||||
from devil.android.device_list import *
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,113 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""Manages intents and associated information.
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
This is generally intended to be used with functions that calls Android's
|
||||
Am command.
|
||||
"""
|
||||
|
||||
class Intent(object):
|
||||
|
||||
def __init__(self, action='android.intent.action.VIEW', activity=None,
|
||||
category=None, component=None, data=None, extras=None,
|
||||
flags=None, package=None):
|
||||
"""Creates an Intent.
|
||||
|
||||
Args:
|
||||
action: A string containing the action.
|
||||
activity: A string that, with |package|, can be used to specify the
|
||||
component.
|
||||
category: A string or list containing any categories.
|
||||
component: A string that specifies the component to send the intent to.
|
||||
data: A string containing a data URI.
|
||||
extras: A dict containing extra parameters to be passed along with the
|
||||
intent.
|
||||
flags: A string containing flags to pass.
|
||||
package: A string that, with activity, can be used to specify the
|
||||
component.
|
||||
"""
|
||||
self._action = action
|
||||
self._activity = activity
|
||||
if isinstance(category, list) or category is None:
|
||||
self._category = category
|
||||
else:
|
||||
self._category = [category]
|
||||
self._component = component
|
||||
self._data = data
|
||||
self._extras = extras
|
||||
self._flags = flags
|
||||
self._package = package
|
||||
|
||||
if self._component and '/' in component:
|
||||
self._package, self._activity = component.split('/', 1)
|
||||
elif self._package and self._activity:
|
||||
self._component = '%s/%s' % (package, activity)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
return self._action
|
||||
|
||||
@property
|
||||
def activity(self):
|
||||
return self._activity
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return self._category
|
||||
|
||||
@property
|
||||
def component(self):
|
||||
return self._component
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def extras(self):
|
||||
return self._extras
|
||||
|
||||
@property
|
||||
def flags(self):
|
||||
return self._flags
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
return self._package
|
||||
|
||||
@property
|
||||
def am_args(self):
|
||||
"""Returns the intent as a list of arguments for the activity manager.
|
||||
|
||||
For details refer to the specification at:
|
||||
- http://developer.android.com/tools/help/adb.html#IntentSpec
|
||||
"""
|
||||
args = []
|
||||
if self.action:
|
||||
args.extend(['-a', self.action])
|
||||
if self.data:
|
||||
args.extend(['-d', self.data])
|
||||
if self.category:
|
||||
args.extend(arg for cat in self.category for arg in ('-c', cat))
|
||||
if self.component:
|
||||
args.extend(['-n', self.component])
|
||||
if self.flags:
|
||||
args.extend(['-f', self.flags])
|
||||
if self.extras:
|
||||
for key, value in self.extras.iteritems():
|
||||
if value is None:
|
||||
args.extend(['--esn', key])
|
||||
elif isinstance(value, str):
|
||||
args.extend(['--es', key, value])
|
||||
elif isinstance(value, bool):
|
||||
args.extend(['--ez', key, str(value)])
|
||||
elif isinstance(value, int):
|
||||
args.extend(['--ei', key, str(value)])
|
||||
elif isinstance(value, float):
|
||||
args.extend(['--ef', key, str(value)])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Intent does not know how to pass %s extras' % type(value))
|
||||
return args
|
||||
from devil.android.sdk.intent import *
|
||||
|
|
|
@ -2,138 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import re
|
||||
|
||||
from pylib.device import adb_wrapper
|
||||
from pylib.device import decorators
|
||||
from pylib.device import device_errors
|
||||
|
||||
|
||||
class LogcatMonitor(object):
|
||||
|
||||
_THREADTIME_RE_FORMAT = (
|
||||
r'(?P<date>\S*) +(?P<time>\S*) +(?P<proc_id>%s) +(?P<thread_id>%s) +'
|
||||
r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')
|
||||
|
||||
def __init__(self, adb, clear=True, filter_specs=None):
|
||||
"""Create a LogcatMonitor instance.
|
||||
|
||||
Args:
|
||||
adb: An instance of adb_wrapper.AdbWrapper.
|
||||
clear: If True, clear the logcat when monitoring starts.
|
||||
filter_specs: An optional list of '<tag>[:priority]' strings.
|
||||
"""
|
||||
if isinstance(adb, adb_wrapper.AdbWrapper):
|
||||
self._adb = adb
|
||||
else:
|
||||
raise ValueError('Unsupported type passed for argument "device"')
|
||||
self._clear = clear
|
||||
self._filter_specs = filter_specs
|
||||
self._logcat_out = None
|
||||
self._logcat_out_file = None
|
||||
self._logcat_proc = None
|
||||
|
||||
@decorators.WithTimeoutAndRetriesDefaults(10, 0)
|
||||
def WaitFor(self, success_regex, failure_regex=None, timeout=None,
|
||||
retries=None):
|
||||
"""Wait for a matching logcat line or until a timeout occurs.
|
||||
|
||||
This will attempt to match lines in the logcat against both |success_regex|
|
||||
and |failure_regex| (if provided). Note that this calls re.search on each
|
||||
logcat line, not re.match, so the provided regular expressions don't have
|
||||
to match an entire line.
|
||||
|
||||
Args:
|
||||
success_regex: The regular expression to search for.
|
||||
failure_regex: An optional regular expression that, if hit, causes this
|
||||
to stop looking for a match. Can be None.
|
||||
timeout: timeout in seconds
|
||||
retries: number of retries
|
||||
|
||||
Returns:
|
||||
A match object if |success_regex| matches a part of a logcat line, or
|
||||
None if |failure_regex| matches a part of a logcat line.
|
||||
Raises:
|
||||
CommandFailedError on logcat failure (NOT on a |failure_regex| match).
|
||||
CommandTimeoutError if no logcat line matching either |success_regex| or
|
||||
|failure_regex| is found in |timeout| seconds.
|
||||
DeviceUnreachableError if the device becomes unreachable.
|
||||
"""
|
||||
if isinstance(success_regex, basestring):
|
||||
success_regex = re.compile(success_regex)
|
||||
if isinstance(failure_regex, basestring):
|
||||
failure_regex = re.compile(failure_regex)
|
||||
|
||||
logging.debug('Waiting %d seconds for "%s"', timeout, success_regex.pattern)
|
||||
|
||||
# NOTE This will continue looping until:
|
||||
# - success_regex matches a line, in which case the match object is
|
||||
# returned.
|
||||
# - failure_regex matches a line, in which case None is returned
|
||||
# - the timeout is hit, in which case a CommandTimeoutError is raised.
|
||||
for l in self._adb.Logcat(filter_specs=self._filter_specs):
|
||||
m = success_regex.search(l)
|
||||
if m:
|
||||
return m
|
||||
if failure_regex and failure_regex.search(l):
|
||||
return None
|
||||
|
||||
def FindAll(self, message_regex, proc_id=None, thread_id=None, log_level=None,
|
||||
component=None):
|
||||
"""Finds all lines in the logcat that match the provided constraints.
|
||||
|
||||
Args:
|
||||
message_regex: The regular expression that the <message> section must
|
||||
match.
|
||||
proc_id: The process ID to match. If None, matches any process ID.
|
||||
thread_id: The thread ID to match. If None, matches any thread ID.
|
||||
log_level: The log level to match. If None, matches any log level.
|
||||
component: The component to match. If None, matches any component.
|
||||
|
||||
Yields:
|
||||
A match object for each matching line in the logcat. The match object
|
||||
will always contain, in addition to groups defined in |message_regex|,
|
||||
the following named groups: 'date', 'time', 'proc_id', 'thread_id',
|
||||
'log_level', 'component', and 'message'.
|
||||
"""
|
||||
if proc_id is None:
|
||||
proc_id = r'\d+'
|
||||
if thread_id is None:
|
||||
thread_id = r'\d+'
|
||||
if log_level is None:
|
||||
log_level = r'[VDIWEF]'
|
||||
if component is None:
|
||||
component = r'[^\s:]+'
|
||||
threadtime_re = re.compile(
|
||||
type(self)._THREADTIME_RE_FORMAT % (
|
||||
proc_id, thread_id, log_level, component, message_regex))
|
||||
|
||||
for line in self._adb.Logcat(dump=True, logcat_format='threadtime'):
|
||||
m = re.match(threadtime_re, line)
|
||||
if m:
|
||||
yield m
|
||||
|
||||
def Start(self):
|
||||
"""Starts the logcat monitor.
|
||||
|
||||
Clears the logcat if |clear| was set in |__init__|.
|
||||
"""
|
||||
if self._clear:
|
||||
self._adb.Logcat(clear=True)
|
||||
|
||||
def __enter__(self):
|
||||
"""Starts the logcat monitor."""
|
||||
self.Start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Stops the logcat monitor."""
|
||||
pass
|
||||
from devil.android.logcat_monitor import *
|
||||
|
|
|
@ -2,390 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Helper object to read and modify Shared Preferences from Android apps.
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
See e.g.:
|
||||
http://developer.android.com/reference/android/content/SharedPreferences.html
|
||||
"""
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import posixpath
|
||||
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
_XML_DECLARATION = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
|
||||
|
||||
|
||||
class BasePref(object):
|
||||
"""Base class for getting/setting the value of a specific preference type.
|
||||
|
||||
Should not be instantiated directly. The SharedPrefs collection will
|
||||
instantiate the appropriate subclasses, which directly manipulate the
|
||||
underlying xml document, to parse and serialize values according to their
|
||||
type.
|
||||
|
||||
Args:
|
||||
elem: An xml ElementTree object holding the preference data.
|
||||
|
||||
Properties:
|
||||
tag_name: A string with the tag that must be used for this preference type.
|
||||
"""
|
||||
tag_name = None
|
||||
|
||||
def __init__(self, elem):
|
||||
if elem.tag != type(self).tag_name:
|
||||
raise TypeError('Property %r has type %r, but trying to access as %r' %
|
||||
(elem.get('name'), elem.tag, type(self).tag_name))
|
||||
self._elem = elem
|
||||
|
||||
def __str__(self):
|
||||
"""Get the underlying xml element as a string."""
|
||||
return ElementTree.tostring(self._elem)
|
||||
|
||||
def get(self):
|
||||
"""Get the value of this preference."""
|
||||
return self._elem.get('value')
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a value casted as a string."""
|
||||
self._elem.set('value', str(value))
|
||||
|
||||
@property
|
||||
def has_value(self):
|
||||
"""Check whether the element has a value."""
|
||||
return self._elem.get('value') is not None
|
||||
|
||||
|
||||
class BooleanPref(BasePref):
|
||||
"""Class for getting/setting a preference with a boolean value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<boolean name="featureEnabled" value="false" />
|
||||
"""
|
||||
tag_name = 'boolean'
|
||||
VALUES = {'true': True, 'false': False}
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python bool."""
|
||||
return type(self).VALUES[super(BooleanPref, self).get()]
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a value casted as a bool."""
|
||||
super(BooleanPref, self).set('true' if value else 'false')
|
||||
|
||||
|
||||
class FloatPref(BasePref):
|
||||
"""Class for getting/setting a preference with a float value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<float name="someMetric" value="4.7" />
|
||||
"""
|
||||
tag_name = 'float'
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python float."""
|
||||
return float(super(FloatPref, self).get())
|
||||
|
||||
|
||||
class IntPref(BasePref):
|
||||
"""Class for getting/setting a preference with an int value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<int name="aCounter" value="1234" />
|
||||
"""
|
||||
tag_name = 'int'
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python int."""
|
||||
return int(super(IntPref, self).get())
|
||||
|
||||
|
||||
class LongPref(IntPref):
|
||||
"""Class for getting/setting a preference with a long value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<long name="aLongCounter" value="1234" />
|
||||
|
||||
We use the same implementation from IntPref.
|
||||
"""
|
||||
tag_name = 'long'
|
||||
|
||||
|
||||
class StringPref(BasePref):
|
||||
"""Class for getting/setting a preference with a string value.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<string name="someHashValue">249b3e5af13d4db2</string>
|
||||
"""
|
||||
tag_name = 'string'
|
||||
|
||||
def get(self):
|
||||
"""Get the value as a Python string."""
|
||||
return self._elem.text
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a value casted as a string."""
|
||||
self._elem.text = str(value)
|
||||
|
||||
|
||||
class StringSetPref(StringPref):
|
||||
"""Class for getting/setting a preference with a set of string values.
|
||||
|
||||
The underlying xml element has the form, e.g.:
|
||||
<set name="managed_apps">
|
||||
<string>com.mine.app1</string>
|
||||
<string>com.mine.app2</string>
|
||||
<string>com.mine.app3</string>
|
||||
</set>
|
||||
"""
|
||||
tag_name = 'set'
|
||||
|
||||
def get(self):
|
||||
"""Get a list with the string values contained."""
|
||||
value = []
|
||||
for child in self._elem:
|
||||
assert child.tag == 'string'
|
||||
value.append(child.text)
|
||||
return value
|
||||
|
||||
def set(self, value):
|
||||
"""Set from a sequence of values, each casted as a string."""
|
||||
for child in list(self._elem):
|
||||
self._elem.remove(child)
|
||||
for item in value:
|
||||
ElementTree.SubElement(self._elem, 'string').text = str(item)
|
||||
|
||||
|
||||
_PREF_TYPES = {c.tag_name: c for c in [BooleanPref, FloatPref, IntPref,
|
||||
LongPref, StringPref, StringSetPref]}
|
||||
|
||||
|
||||
class SharedPrefs(object):
|
||||
def __init__(self, device, package, filename):
|
||||
"""Helper object to read and update "Shared Prefs" of Android apps.
|
||||
|
||||
Such files typically look like, e.g.:
|
||||
|
||||
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
||||
<map>
|
||||
<int name="databaseVersion" value="107" />
|
||||
<boolean name="featureEnabled" value="false" />
|
||||
<string name="someHashValue">249b3e5af13d4db2</string>
|
||||
</map>
|
||||
|
||||
Example usage:
|
||||
|
||||
prefs = shared_prefs.SharedPrefs(device, 'com.my.app', 'my_prefs.xml')
|
||||
prefs.Load()
|
||||
prefs.GetString('someHashValue') # => '249b3e5af13d4db2'
|
||||
prefs.SetInt('databaseVersion', 42)
|
||||
prefs.Remove('featureEnabled')
|
||||
prefs.Commit()
|
||||
|
||||
The object may also be used as a context manager to automatically load and
|
||||
commit, respectively, upon entering and leaving the context.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils object.
|
||||
package: A string with the package name of the app that owns the shared
|
||||
preferences file.
|
||||
filename: A string with the name of the preferences file to read/write.
|
||||
"""
|
||||
self._device = device
|
||||
self._xml = None
|
||||
self._package = package
|
||||
self._filename = filename
|
||||
self._path = '/data/data/%s/shared_prefs/%s' % (package, filename)
|
||||
self._changed = False
|
||||
|
||||
def __repr__(self):
|
||||
"""Get a useful printable representation of the object."""
|
||||
return '<{cls} file {filename} for {package} on {device}>'.format(
|
||||
cls=type(self).__name__, filename=self.filename, package=self.package,
|
||||
device=str(self._device))
|
||||
|
||||
def __str__(self):
|
||||
"""Get the underlying xml document as a string."""
|
||||
return _XML_DECLARATION + ElementTree.tostring(self.xml)
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
"""Get the package name of the app that owns the shared preferences."""
|
||||
return self._package
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
"""Get the filename of the shared preferences file."""
|
||||
return self._filename
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Get the full path to the shared preferences file on the device."""
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""True if properties have changed and a commit would be needed."""
|
||||
return self._changed
|
||||
|
||||
@property
|
||||
def xml(self):
|
||||
"""Get the underlying xml document as an ElementTree object."""
|
||||
if self._xml is None:
|
||||
self._xml = ElementTree.Element('map')
|
||||
return self._xml
|
||||
|
||||
def Load(self):
|
||||
"""Load the shared preferences file from the device.
|
||||
|
||||
A empty xml document, which may be modified and saved on |commit|, is
|
||||
created if the file does not already exist.
|
||||
"""
|
||||
if self._device.FileExists(self.path):
|
||||
self._xml = ElementTree.fromstring(
|
||||
self._device.ReadFile(self.path, as_root=True))
|
||||
assert self._xml.tag == 'map'
|
||||
else:
|
||||
self._xml = None
|
||||
self._changed = False
|
||||
|
||||
def Clear(self):
|
||||
"""Clear all of the preferences contained in this object."""
|
||||
if self._xml is not None and len(self): # only clear if not already empty
|
||||
self._xml = None
|
||||
self._changed = True
|
||||
|
||||
def Commit(self):
|
||||
"""Save the current set of preferences to the device.
|
||||
|
||||
Only actually saves if some preferences have been modified.
|
||||
"""
|
||||
if not self.changed:
|
||||
return
|
||||
self._device.RunShellCommand(
|
||||
['mkdir', '-p', posixpath.dirname(self.path)],
|
||||
as_root=True, check_return=True)
|
||||
self._device.WriteFile(self.path, str(self), as_root=True)
|
||||
self._device.KillAll(self.package, exact=True, as_root=True, quiet=True)
|
||||
self._changed = False
|
||||
|
||||
def __len__(self):
|
||||
"""Get the number of preferences in this collection."""
|
||||
return len(self.xml)
|
||||
|
||||
def PropertyType(self, key):
|
||||
"""Get the type (i.e. tag name) of a property in the collection."""
|
||||
return self._GetChild(key).tag
|
||||
|
||||
def HasProperty(self, key):
|
||||
try:
|
||||
self._GetChild(key)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def GetBoolean(self, key):
|
||||
"""Get a boolean property."""
|
||||
return BooleanPref(self._GetChild(key)).get()
|
||||
|
||||
def SetBoolean(self, key, value):
|
||||
"""Set a boolean property."""
|
||||
self._SetPrefValue(key, value, BooleanPref)
|
||||
|
||||
def GetFloat(self, key):
|
||||
"""Get a float property."""
|
||||
return FloatPref(self._GetChild(key)).get()
|
||||
|
||||
def SetFloat(self, key, value):
|
||||
"""Set a float property."""
|
||||
self._SetPrefValue(key, value, FloatPref)
|
||||
|
||||
def GetInt(self, key):
|
||||
"""Get an int property."""
|
||||
return IntPref(self._GetChild(key)).get()
|
||||
|
||||
def SetInt(self, key, value):
|
||||
"""Set an int property."""
|
||||
self._SetPrefValue(key, value, IntPref)
|
||||
|
||||
def GetLong(self, key):
|
||||
"""Get a long property."""
|
||||
return LongPref(self._GetChild(key)).get()
|
||||
|
||||
def SetLong(self, key, value):
|
||||
"""Set a long property."""
|
||||
self._SetPrefValue(key, value, LongPref)
|
||||
|
||||
def GetString(self, key):
|
||||
"""Get a string property."""
|
||||
return StringPref(self._GetChild(key)).get()
|
||||
|
||||
def SetString(self, key, value):
|
||||
"""Set a string property."""
|
||||
self._SetPrefValue(key, value, StringPref)
|
||||
|
||||
def GetStringSet(self, key):
|
||||
"""Get a string set property."""
|
||||
return StringSetPref(self._GetChild(key)).get()
|
||||
|
||||
def SetStringSet(self, key, value):
|
||||
"""Set a string set property."""
|
||||
self._SetPrefValue(key, value, StringSetPref)
|
||||
|
||||
def Remove(self, key):
|
||||
"""Remove a preference from the collection."""
|
||||
self.xml.remove(self._GetChild(key))
|
||||
|
||||
def AsDict(self):
|
||||
"""Return the properties and their values as a dictionary."""
|
||||
d = {}
|
||||
for child in self.xml:
|
||||
pref = _PREF_TYPES[child.tag](child)
|
||||
d[child.get('name')] = pref.get()
|
||||
return d
|
||||
|
||||
def __enter__(self):
|
||||
"""Load preferences file from the device when entering a context."""
|
||||
self.Load()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, _exc_value, _traceback):
|
||||
"""Save preferences file to the device when leaving a context."""
|
||||
if not exc_type:
|
||||
self.Commit()
|
||||
|
||||
def _GetChild(self, key):
|
||||
"""Get the underlying xml node that holds the property of a given key.
|
||||
|
||||
Raises:
|
||||
KeyError when the key is not found in the collection.
|
||||
"""
|
||||
for child in self.xml:
|
||||
if child.get('name') == key:
|
||||
return child
|
||||
raise KeyError(key)
|
||||
|
||||
def _SetPrefValue(self, key, value, pref_cls):
|
||||
"""Set the value of a property.
|
||||
|
||||
Args:
|
||||
key: The key of the property to set.
|
||||
value: The new value of the property.
|
||||
pref_cls: A subclass of BasePref used to access the property.
|
||||
|
||||
Raises:
|
||||
TypeError when the key already exists but with a different type.
|
||||
"""
|
||||
try:
|
||||
pref = pref_cls(self._GetChild(key))
|
||||
old_value = pref.get()
|
||||
except KeyError:
|
||||
pref = pref_cls(ElementTree.SubElement(
|
||||
self.xml, pref_cls.tag_name, {'name': key}))
|
||||
old_value = None
|
||||
if old_value != value:
|
||||
pref.set(value)
|
||||
self._changed = True
|
||||
logging.info('Setting property: %s', pref)
|
||||
from devil.android.sdk.shared_prefs import *
|
||||
|
|
|
@ -2,40 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Defines constants for signals that should be supported on devices.
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
Note: Obtained by running `kill -l` on a user device.
|
||||
"""
|
||||
|
||||
|
||||
SIGHUP = 1 # Hangup
|
||||
SIGINT = 2 # Interrupt
|
||||
SIGQUIT = 3 # Quit
|
||||
SIGILL = 4 # Illegal instruction
|
||||
SIGTRAP = 5 # Trap
|
||||
SIGABRT = 6 # Aborted
|
||||
SIGBUS = 7 # Bus error
|
||||
SIGFPE = 8 # Floating point exception
|
||||
SIGKILL = 9 # Killed
|
||||
SIGUSR1 = 10 # User signal 1
|
||||
SIGSEGV = 11 # Segmentation fault
|
||||
SIGUSR2 = 12 # User signal 2
|
||||
SIGPIPE = 13 # Broken pipe
|
||||
SIGALRM = 14 # Alarm clock
|
||||
SIGTERM = 15 # Terminated
|
||||
SIGSTKFLT = 16 # Stack fault
|
||||
SIGCHLD = 17 # Child exited
|
||||
SIGCONT = 18 # Continue
|
||||
SIGSTOP = 19 # Stopped (signal)
|
||||
SIGTSTP = 20 # Stopped
|
||||
SIGTTIN = 21 # Stopped (tty input)
|
||||
SIGTTOU = 22 # Stopped (tty output)
|
||||
SIGURG = 23 # Urgent I/O condition
|
||||
SIGXCPU = 24 # CPU time limit exceeded
|
||||
SIGXFSZ = 25 # File size limit exceeded
|
||||
SIGVTALRM = 26 # Virtual timer expired
|
||||
SIGPROF = 27 # Profiling timer expired
|
||||
SIGWINCH = 28 # Window size changed
|
||||
SIGIO = 29 # I/O possible
|
||||
SIGPWR = 30 # Power failure
|
||||
SIGSYS = 31 # Bad system call
|
||||
from devil.android.device_signal import *
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
import logging
|
||||
|
||||
import pylib.device.device_utils
|
||||
|
||||
from pylib.device import device_errors
|
||||
|
||||
|
||||
|
|
|
@ -2,41 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""This module wraps the Android Asset Packaging Tool."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import os
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.utils import timeout_retry
|
||||
|
||||
_AAPT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'aapt')
|
||||
|
||||
def _RunAaptCmd(args):
|
||||
"""Runs an aapt command.
|
||||
|
||||
Args:
|
||||
args: A list of arguments for aapt.
|
||||
|
||||
Returns:
|
||||
The output of the command.
|
||||
"""
|
||||
cmd = [_AAPT_PATH] + args
|
||||
status, output = cmd_helper.GetCmdStatusAndOutput(cmd)
|
||||
if status != 0:
|
||||
raise Exception('Failed running aapt command: "%s" with output "%s".' %
|
||||
(' '.join(cmd), output))
|
||||
return output
|
||||
|
||||
def Dump(what, apk, assets=None):
|
||||
"""Returns the output of the aapt dump command.
|
||||
|
||||
Args:
|
||||
what: What you want to dump.
|
||||
apk: Path to apk you want to dump information for.
|
||||
assets: List of assets in apk you want to dump information for.
|
||||
"""
|
||||
assets = assets or []
|
||||
if isinstance(assets, basestring):
|
||||
assets = [assets]
|
||||
return _RunAaptCmd(['dump', what, apk] + assets).splitlines()
|
||||
from devil.android.sdk.aapt import *
|
||||
|
|
|
@ -2,29 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import os
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
|
||||
_DEXDUMP_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'dexdump')
|
||||
|
||||
def DexDump(dexfiles, file_summary=False):
|
||||
"""A wrapper around the Android SDK's dexdump tool.
|
||||
|
||||
Args:
|
||||
dexfiles: The dexfile or list of dex files to dump.
|
||||
file_summary: Display summary information from the file header. (-f)
|
||||
|
||||
Returns:
|
||||
An iterable over the output lines.
|
||||
"""
|
||||
# TODO(jbudorick): Add support for more options as necessary.
|
||||
if isinstance(dexfiles, basestring):
|
||||
dexfiles = [dexfiles]
|
||||
args = [_DEXDUMP_PATH] + dexfiles
|
||||
if file_summary:
|
||||
args.append('-f')
|
||||
|
||||
return cmd_helper.IterCmdOutputLines(args)
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.android.sdk.dexdump import *
|
||||
|
|
|
@ -2,57 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""This module wraps Android's split-select tool."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import os
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.utils import timeout_retry
|
||||
|
||||
_SPLIT_SELECT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'split-select')
|
||||
|
||||
def _RunSplitSelectCmd(args):
|
||||
"""Runs a split-select command.
|
||||
|
||||
Args:
|
||||
args: A list of arguments for split-select.
|
||||
|
||||
Returns:
|
||||
The output of the command.
|
||||
"""
|
||||
cmd = [_SPLIT_SELECT_PATH] + args
|
||||
status, output = cmd_helper.GetCmdStatusAndOutput(cmd)
|
||||
if status != 0:
|
||||
raise Exception('Failed running command "%s" with output "%s".' %
|
||||
(' '.join(cmd), output))
|
||||
return output
|
||||
|
||||
def _SplitConfig(device):
|
||||
"""Returns a config specifying which APK splits are required by the device.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils object.
|
||||
"""
|
||||
return ('%s-r%s-%s:%s' %
|
||||
(device.language,
|
||||
device.country,
|
||||
device.screen_density,
|
||||
device.product_cpu_abi))
|
||||
|
||||
def SelectSplits(device, base_apk, split_apks):
|
||||
"""Determines which APK splits the device requires.
|
||||
|
||||
Args:
|
||||
device: A DeviceUtils object.
|
||||
base_apk: The path of the base APK.
|
||||
split_apks: A list of paths of APK splits.
|
||||
|
||||
Returns:
|
||||
The list of APK splits that the device requires.
|
||||
"""
|
||||
config = _SplitConfig(device)
|
||||
args = ['--target', config, '--base', base_apk]
|
||||
for split in split_apks:
|
||||
args.extend(['--split', split])
|
||||
return _RunSplitSelectCmd(args).splitlines()
|
||||
from devil.android.sdk.split_select import *
|
||||
|
|
|
@ -1,131 +1,8 @@
|
|||
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""Module containing utilities for apk packages."""
|
||||
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.sdk import aapt
|
||||
|
||||
|
||||
_AAPT_PATH = os.path.join(constants.ANDROID_SDK_TOOLS, 'aapt')
|
||||
_MANIFEST_ATTRIBUTE_RE = re.compile(
|
||||
r'\s*A: ([^\(\)= ]*)\([^\(\)= ]*\)="(.*)" \(Raw: .*\)$')
|
||||
_MANIFEST_ELEMENT_RE = re.compile(r'\s*(?:E|N): (\S*) .*$')
|
||||
_PACKAGE_NAME_RE = re.compile(r'package: .*name=\'(\S*)\'')
|
||||
_SPLIT_NAME_RE = re.compile(r'package: .*split=\'(\S*)\'')
|
||||
|
||||
|
||||
def GetPackageName(apk_path):
|
||||
"""Returns the package name of the apk."""
|
||||
return ApkHelper(apk_path).GetPackageName()
|
||||
|
||||
|
||||
# TODO(jbudorick): Deprecate and remove this function once callers have been
|
||||
# converted to ApkHelper.GetInstrumentationName
|
||||
def GetInstrumentationName(apk_path):
|
||||
"""Returns the name of the Instrumentation in the apk."""
|
||||
return ApkHelper(apk_path).GetInstrumentationName()
|
||||
|
||||
|
||||
def _ParseManifestFromApk(apk_path):
|
||||
aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml')
|
||||
|
||||
parsed_manifest = {}
|
||||
node_stack = [parsed_manifest]
|
||||
indent = ' '
|
||||
|
||||
for line in aapt_output[1:]:
|
||||
if len(line) == 0:
|
||||
continue
|
||||
|
||||
indent_depth = 0
|
||||
while line[(len(indent) * indent_depth):].startswith(indent):
|
||||
indent_depth += 1
|
||||
|
||||
node_stack = node_stack[:indent_depth]
|
||||
node = node_stack[-1]
|
||||
|
||||
m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:])
|
||||
if m:
|
||||
if not m.group(1) in node:
|
||||
node[m.group(1)] = {}
|
||||
node_stack += [node[m.group(1)]]
|
||||
continue
|
||||
|
||||
m = _MANIFEST_ATTRIBUTE_RE.match(line[len(indent) * indent_depth:])
|
||||
if m:
|
||||
if not m.group(1) in node:
|
||||
node[m.group(1)] = []
|
||||
node[m.group(1)].append(m.group(2))
|
||||
continue
|
||||
|
||||
return parsed_manifest
|
||||
|
||||
|
||||
class ApkHelper(object):
|
||||
def __init__(self, apk_path):
|
||||
self._apk_path = apk_path
|
||||
self._manifest = None
|
||||
self._package_name = None
|
||||
self._split_name = None
|
||||
|
||||
def GetActivityName(self):
|
||||
"""Returns the name of the Activity in the apk."""
|
||||
manifest_info = self._GetManifest()
|
||||
try:
|
||||
activity = (
|
||||
manifest_info['manifest']['application']['activity']
|
||||
['android:name'][0])
|
||||
except KeyError:
|
||||
return None
|
||||
if '.' not in activity:
|
||||
activity = '%s.%s' % (self.GetPackageName(), activity)
|
||||
elif activity.startswith('.'):
|
||||
activity = '%s%s' % (self.GetPackageName(), activity)
|
||||
return activity
|
||||
|
||||
def GetInstrumentationName(
|
||||
self, default='android.test.InstrumentationTestRunner'):
|
||||
"""Returns the name of the Instrumentation in the apk."""
|
||||
manifest_info = self._GetManifest()
|
||||
try:
|
||||
return manifest_info['manifest']['instrumentation']['android:name'][0]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def GetPackageName(self):
|
||||
"""Returns the package name of the apk."""
|
||||
if self._package_name:
|
||||
return self._package_name
|
||||
|
||||
aapt_output = aapt.Dump('badging', self._apk_path)
|
||||
for line in aapt_output:
|
||||
m = _PACKAGE_NAME_RE.match(line)
|
||||
if m:
|
||||
self._package_name = m.group(1)
|
||||
return self._package_name
|
||||
raise Exception('Failed to determine package name of %s' % self._apk_path)
|
||||
|
||||
def GetSplitName(self):
|
||||
"""Returns the name of the split of the apk."""
|
||||
if self._split_name:
|
||||
return self._split_name
|
||||
|
||||
aapt_output = aapt.Dump('badging', self._apk_path)
|
||||
for line in aapt_output:
|
||||
m = _SPLIT_NAME_RE.match(line)
|
||||
if m:
|
||||
self._split_name = m.group(1)
|
||||
return self._split_name
|
||||
return None
|
||||
|
||||
def _GetManifest(self):
|
||||
if not self._manifest:
|
||||
self._manifest = _ParseManifestFromApk(self._apk_path)
|
||||
return self._manifest
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.android.apk_helper import *
|
||||
|
|
|
@ -2,15 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
class BaseError(Exception):
|
||||
"""Base error for all test runner errors."""
|
||||
|
||||
def __init__(self, message, is_infra_error=False):
|
||||
super(BaseError, self).__init__(message)
|
||||
self._is_infra_error = is_infra_error
|
||||
|
||||
@property
|
||||
def is_infra_error(self):
|
||||
"""Property to indicate if error was caused by an infrastructure issue."""
|
||||
return self._is_infra_error
|
||||
from devil.base_error import *
|
||||
|
|
|
@ -1,63 +1,8 @@
|
|||
# Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""A temp file that automatically gets pushed and deleted from a device."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
# pylint: disable=W0622
|
||||
|
||||
import threading
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib.device import device_errors
|
||||
|
||||
_COMMAND_TEMPLATE = (
|
||||
# Make sure that the temp dir is writable
|
||||
'test -d {dir} && '
|
||||
# If 5 random attempts fail, something is up.
|
||||
'for i in 1 2 3 4 5; do '
|
||||
'fn={dir}/{prefix}-$(date +%s)-"$RANDOM"{suffix};'
|
||||
'test -e "$fn" || break;'
|
||||
'done && '
|
||||
# Touch the file, so other temp files can't get the same name.
|
||||
'touch "$fn" && echo -n "$fn"')
|
||||
|
||||
class DeviceTempFile(object):
|
||||
def __init__(self, adb, suffix='', prefix='temp_file', dir='/data/local/tmp'):
|
||||
"""Find an unused temporary file path in the devices external directory.
|
||||
|
||||
When this object is closed, the file will be deleted on the device.
|
||||
|
||||
Args:
|
||||
adb: An instance of AdbWrapper
|
||||
suffix: The suffix of the name of the temp file.
|
||||
prefix: The prefix of the name of the temp file.
|
||||
dir: The directory on the device where to place the temp file.
|
||||
"""
|
||||
self._adb = adb
|
||||
command = _COMMAND_TEMPLATE.format(
|
||||
dir=cmd_helper.SingleQuote(dir),
|
||||
suffix=cmd_helper.SingleQuote(suffix),
|
||||
prefix=cmd_helper.SingleQuote(prefix))
|
||||
self.name = self._adb.Shell(command)
|
||||
self.name_quoted = cmd_helper.SingleQuote(self.name)
|
||||
|
||||
def close(self):
|
||||
"""Deletes the temporary file from the device."""
|
||||
# ignore exception if the file is already gone.
|
||||
def helper():
|
||||
try:
|
||||
self._adb.Shell('rm -f %s' % self.name_quoted, expect_status=None)
|
||||
except device_errors.AdbCommandFailedError:
|
||||
# file does not exist on Android version without 'rm -f' support (ICS)
|
||||
pass
|
||||
|
||||
# It shouldn't matter when the temp file gets deleted, so do so
|
||||
# asynchronously.
|
||||
threading.Thread(target=helper).start()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.close()
|
||||
from devil.android.device_temp_file import *
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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
|
||||
|
||||
|
||||
def GetRecursiveDiskUsage(path):
|
||||
"""Returns the disk usage in bytes of |path|. Similar to `du -sb |path|`."""
|
||||
running_size = os.path.getsize(path)
|
||||
if os.path.isdir(path):
|
||||
for root, dirs, files in os.walk(path):
|
||||
running_size += sum([os.path.getsize(os.path.join(root, f))
|
||||
for f in files + dirs])
|
||||
return running_size
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.utils.host_utils import *
|
||||
|
|
|
@ -1,115 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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 collections
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib.utils import device_temp_file
|
||||
from pylib.device import device_errors
|
||||
|
||||
MD5SUM_DEVICE_LIB_PATH = '/data/local/tmp/md5sum/'
|
||||
MD5SUM_DEVICE_BIN_PATH = MD5SUM_DEVICE_LIB_PATH + 'md5sum_bin'
|
||||
|
||||
_STARTS_WITH_CHECKSUM_RE = re.compile(r'^\s*[0-9a-fA-F]{32}\s+')
|
||||
|
||||
|
||||
def CalculateHostMd5Sums(paths):
|
||||
"""Calculates the MD5 sum value for all items in |paths|.
|
||||
|
||||
Directories are traversed recursively and the MD5 sum of each file found is
|
||||
reported in the result.
|
||||
|
||||
Args:
|
||||
paths: A list of host paths to md5sum.
|
||||
Returns:
|
||||
A dict mapping file paths to their respective md5sum checksums.
|
||||
"""
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
|
||||
md5sum_bin_host_path = os.path.join(
|
||||
constants.GetOutDirectory(), 'md5sum_bin_host')
|
||||
if not os.path.exists(md5sum_bin_host_path):
|
||||
raise IOError('File not built: %s' % md5sum_bin_host_path)
|
||||
out = cmd_helper.GetCmdOutput([md5sum_bin_host_path] + [p for p in paths])
|
||||
|
||||
return _ParseMd5SumOutput(out.splitlines())
|
||||
|
||||
|
||||
def CalculateDeviceMd5Sums(paths, device):
|
||||
"""Calculates the MD5 sum value for all items in |paths|.
|
||||
|
||||
Directories are traversed recursively and the MD5 sum of each file found is
|
||||
reported in the result.
|
||||
|
||||
Args:
|
||||
paths: A list of device paths to md5sum.
|
||||
Returns:
|
||||
A dict mapping file paths to their respective md5sum checksums.
|
||||
"""
|
||||
if not paths:
|
||||
return {}
|
||||
|
||||
if isinstance(paths, basestring):
|
||||
paths = [paths]
|
||||
# Allow generators
|
||||
paths = list(paths)
|
||||
|
||||
md5sum_dist_path = os.path.join(constants.GetOutDirectory(), 'md5sum_dist')
|
||||
md5sum_dist_bin_path = os.path.join(md5sum_dist_path, 'md5sum_bin')
|
||||
|
||||
if not os.path.exists(md5sum_dist_path):
|
||||
raise IOError('File not built: %s' % md5sum_dist_path)
|
||||
md5sum_file_size = os.path.getsize(md5sum_dist_bin_path)
|
||||
|
||||
# For better performance, make the script as small as possible to try and
|
||||
# avoid needing to write to an intermediary file (which RunShellCommand will
|
||||
# do if necessary).
|
||||
md5sum_script = 'a=%s;' % MD5SUM_DEVICE_BIN_PATH
|
||||
# Check if the binary is missing or has changed (using its file size as an
|
||||
# indicator), and trigger a (re-)push via the exit code.
|
||||
md5sum_script += '! [[ $(ls -l $a) = *%d* ]]&&exit 2;' % md5sum_file_size
|
||||
# Make sure it can find libbase.so
|
||||
md5sum_script += 'export LD_LIBRARY_PATH=%s;' % MD5SUM_DEVICE_LIB_PATH
|
||||
if len(paths) > 1:
|
||||
prefix = posixpath.commonprefix(paths)
|
||||
if len(prefix) > 4:
|
||||
md5sum_script += 'p="%s";' % prefix
|
||||
paths = ['$p"%s"' % p[len(prefix):] for p in paths]
|
||||
|
||||
md5sum_script += ';'.join('$a %s' % p for p in paths)
|
||||
# Don't fail the script if the last md5sum fails (due to file not found)
|
||||
# Note: ":" is equivalent to "true".
|
||||
md5sum_script += ';:'
|
||||
try:
|
||||
out = device.RunShellCommand(md5sum_script, check_return=True)
|
||||
except device_errors.AdbShellCommandFailedError as e:
|
||||
# Push the binary only if it is found to not exist
|
||||
# (faster than checking up-front).
|
||||
if e.status == 2:
|
||||
# If files were previously pushed as root (adbd running as root), trying
|
||||
# to re-push as non-root causes the push command to report success, but
|
||||
# actually fail. So, wipe the directory first.
|
||||
device.RunShellCommand(['rm', '-rf', MD5SUM_DEVICE_LIB_PATH],
|
||||
as_root=True, check_return=True)
|
||||
device.adb.Push(md5sum_dist_path, MD5SUM_DEVICE_LIB_PATH)
|
||||
out = device.RunShellCommand(md5sum_script, check_return=True)
|
||||
else:
|
||||
raise
|
||||
|
||||
return _ParseMd5SumOutput(out)
|
||||
|
||||
|
||||
def _ParseMd5SumOutput(out):
|
||||
hash_and_path = (l.split(None, 1) for l in out
|
||||
if l and _STARTS_WITH_CHECKSUM_RE.match(l))
|
||||
return dict((p, h) for h, p in hash_and_path)
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.android.md5sum import *
|
||||
|
|
|
@ -1,182 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""
|
||||
A test facility to assert call sequences while mocking their behavior.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from pylib import constants
|
||||
|
||||
sys.path.append(os.path.join(
|
||||
constants.DIR_SOURCE_ROOT, 'third_party', 'pymock'))
|
||||
import mock # pylint: disable=F0401
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""Adds assertCalls to TestCase objects."""
|
||||
class _AssertCalls(object):
|
||||
def __init__(self, test_case, expected_calls, watched):
|
||||
def call_action(pair):
|
||||
if isinstance(pair, type(mock.call)):
|
||||
return (pair, None)
|
||||
else:
|
||||
return pair
|
||||
|
||||
def do_check(call):
|
||||
def side_effect(*args, **kwargs):
|
||||
received_call = call(*args, **kwargs)
|
||||
self._test_case.assertTrue(
|
||||
self._expected_calls,
|
||||
msg=('Unexpected call: %s' % str(received_call)))
|
||||
expected_call, action = self._expected_calls.pop(0)
|
||||
self._test_case.assertTrue(
|
||||
received_call == expected_call,
|
||||
msg=('Expected call mismatch:\n'
|
||||
' expected: %s\n'
|
||||
' received: %s\n'
|
||||
% (str(expected_call), str(received_call))))
|
||||
if callable(action):
|
||||
return action(*args, **kwargs)
|
||||
else:
|
||||
return action
|
||||
return side_effect
|
||||
|
||||
self._test_case = test_case
|
||||
self._expected_calls = [call_action(pair) for pair in expected_calls]
|
||||
watched = watched.copy() # do not pollute the caller's dict
|
||||
watched.update((call.parent.name, call.parent)
|
||||
for call, _ in self._expected_calls)
|
||||
self._patched = [test_case.patch_call(call, side_effect=do_check(call))
|
||||
for call in watched.itervalues()]
|
||||
|
||||
def __enter__(self):
|
||||
for patch in self._patched:
|
||||
patch.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
for patch in self._patched:
|
||||
patch.__exit__(exc_type, exc_val, exc_tb)
|
||||
if exc_type is None:
|
||||
missing = ''.join(' expected: %s\n' % str(call)
|
||||
for call, _ in self._expected_calls)
|
||||
self._test_case.assertFalse(
|
||||
missing,
|
||||
msg='Expected calls not found:\n' + missing)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TestCase, self).__init__(*args, **kwargs)
|
||||
self.call = mock.call.self
|
||||
self._watched = {}
|
||||
|
||||
def call_target(self, call):
|
||||
"""Resolve a self.call instance to the target it represents.
|
||||
|
||||
Args:
|
||||
call: a self.call instance, e.g. self.call.adb.Shell
|
||||
|
||||
Returns:
|
||||
The target object represented by the call, e.g. self.adb.Shell
|
||||
|
||||
Raises:
|
||||
ValueError if the path of the call does not start with "self", i.e. the
|
||||
target of the call is external to the self object.
|
||||
AttributeError if the path of the call does not specify a valid
|
||||
chain of attributes (without any calls) starting from "self".
|
||||
"""
|
||||
path = call.name.split('.')
|
||||
if path.pop(0) != 'self':
|
||||
raise ValueError("Target %r outside of 'self' object" % call.name)
|
||||
target = self
|
||||
for attr in path:
|
||||
target = getattr(target, attr)
|
||||
return target
|
||||
|
||||
def patch_call(self, call, **kwargs):
|
||||
"""Patch the target of a mock.call instance.
|
||||
|
||||
Args:
|
||||
call: a mock.call instance identifying a target to patch
|
||||
Extra keyword arguments are processed by mock.patch
|
||||
|
||||
Returns:
|
||||
A context manager to mock/unmock the target of the call
|
||||
"""
|
||||
if call.name.startswith('self.'):
|
||||
target = self.call_target(call.parent)
|
||||
_, attribute = call.name.rsplit('.', 1)
|
||||
if (hasattr(type(target), attribute)
|
||||
and isinstance(getattr(type(target), attribute), property)):
|
||||
return mock.patch.object(
|
||||
type(target), attribute, new_callable=mock.PropertyMock, **kwargs)
|
||||
else:
|
||||
return mock.patch.object(target, attribute, **kwargs)
|
||||
else:
|
||||
return mock.patch(call.name, **kwargs)
|
||||
|
||||
def watchCalls(self, calls):
|
||||
"""Add calls to the set of watched calls.
|
||||
|
||||
Args:
|
||||
calls: a sequence of mock.call instances identifying targets to watch
|
||||
"""
|
||||
self._watched.update((call.name, call) for call in calls)
|
||||
|
||||
def watchMethodCalls(self, call, ignore=None):
|
||||
"""Watch all public methods of the target identified by a self.call.
|
||||
|
||||
Args:
|
||||
call: a self.call instance indetifying an object
|
||||
ignore: a list of public methods to ignore when watching for calls
|
||||
"""
|
||||
target = self.call_target(call)
|
||||
if ignore is None:
|
||||
ignore = []
|
||||
self.watchCalls(getattr(call, method)
|
||||
for method in dir(target.__class__)
|
||||
if not method.startswith('_') and not method in ignore)
|
||||
|
||||
def clearWatched(self):
|
||||
"""Clear the set of watched calls."""
|
||||
self._watched = {}
|
||||
|
||||
def assertCalls(self, *calls):
|
||||
"""A context manager to assert that a sequence of calls is made.
|
||||
|
||||
During the assertion, a number of functions and methods will be "watched",
|
||||
and any calls made to them is expected to appear---in the exact same order,
|
||||
and with the exact same arguments---as specified by the argument |calls|.
|
||||
|
||||
By default, the targets of all expected calls are watched. Further targets
|
||||
to watch may be added using watchCalls and watchMethodCalls.
|
||||
|
||||
Optionaly, each call may be accompanied by an action. If the action is a
|
||||
(non-callable) value, this value will be used as the return value given to
|
||||
the caller when the matching call is found. Alternatively, if the action is
|
||||
a callable, the action will be then called with the same arguments as the
|
||||
intercepted call, so that it can provide a return value or perform other
|
||||
side effects. If the action is missing, a return value of None is assumed.
|
||||
|
||||
Note that mock.Mock objects are often convenient to use as a callable
|
||||
action, e.g. to raise exceptions or return other objects which are
|
||||
themselves callable.
|
||||
|
||||
Args:
|
||||
calls: each argument is either a pair (expected_call, action) or just an
|
||||
expected_call, where expected_call is a mock.call instance.
|
||||
|
||||
Raises:
|
||||
AssertionError if the watched targets do not receive the exact sequence
|
||||
of calls specified. Missing calls, extra calls, and calls with
|
||||
mismatching arguments, all cause the assertion to fail.
|
||||
"""
|
||||
return self._AssertCalls(self, calls, self._watched)
|
||||
|
||||
def assertCall(self, call, action=None):
|
||||
return self.assertCalls((call, action))
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.utils.mock_calls import *
|
||||
|
|
|
@ -1,242 +1,8 @@
|
|||
# Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
""" Wrapper that allows method execution in parallel.
|
||||
|
||||
This class wraps a list of objects of the same type, emulates their
|
||||
interface, and executes any functions called on the objects in parallel
|
||||
in ReraiserThreads.
|
||||
|
||||
This means that, given a list of objects:
|
||||
|
||||
class Foo:
|
||||
def __init__(self):
|
||||
self.baz = Baz()
|
||||
|
||||
def bar(self, my_param):
|
||||
// do something
|
||||
|
||||
list_of_foos = [Foo(1), Foo(2), Foo(3)]
|
||||
|
||||
we can take a sequential operation on that list of objects:
|
||||
|
||||
for f in list_of_foos:
|
||||
f.bar('Hello')
|
||||
|
||||
and run it in parallel across all of the objects:
|
||||
|
||||
Parallelizer(list_of_foos).bar('Hello')
|
||||
|
||||
It can also handle (non-method) attributes of objects, so that this:
|
||||
|
||||
for f in list_of_foos:
|
||||
f.baz.myBazMethod()
|
||||
|
||||
can be run in parallel with:
|
||||
|
||||
Parallelizer(list_of_foos).baz.myBazMethod()
|
||||
|
||||
Because it emulates the interface of the wrapped objects, a Parallelizer
|
||||
can be passed to a method or function that takes objects of that type:
|
||||
|
||||
def DoesSomethingWithFoo(the_foo):
|
||||
the_foo.bar('Hello')
|
||||
the_foo.bar('world')
|
||||
the_foo.baz.myBazMethod
|
||||
|
||||
DoesSomethingWithFoo(Parallelizer(list_of_foos))
|
||||
|
||||
Note that this class spins up a thread for each object. Using this class
|
||||
to parallelize operations that are already fast will incur a net performance
|
||||
penalty.
|
||||
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
from pylib.utils import reraiser_thread
|
||||
from pylib.utils import watchdog_timer
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_RETRIES = 3
|
||||
|
||||
|
||||
class Parallelizer(object):
|
||||
"""Allows parallel execution of method calls across a group of objects."""
|
||||
|
||||
def __init__(self, objs):
|
||||
assert (objs is not None and len(objs) > 0), (
|
||||
"Passed empty list to 'Parallelizer'")
|
||||
self._orig_objs = objs
|
||||
self._objs = objs
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Emulate getting the |name| attribute of |self|.
|
||||
|
||||
Args:
|
||||
name: The name of the attribute to retrieve.
|
||||
Returns:
|
||||
A Parallelizer emulating the |name| attribute of |self|.
|
||||
"""
|
||||
self.pGet(None)
|
||||
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = [getattr(o, name) for o in self._objs]
|
||||
return r
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Emulate getting the value of |self| at |index|.
|
||||
|
||||
Returns:
|
||||
A Parallelizer emulating the value of |self| at |index|.
|
||||
"""
|
||||
self.pGet(None)
|
||||
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = [o[index] for o in self._objs]
|
||||
return r
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Emulate calling |self| with |args| and |kwargs|.
|
||||
|
||||
Note that this call is asynchronous. Call pFinish on the return value to
|
||||
block until the call finishes.
|
||||
|
||||
Returns:
|
||||
A Parallelizer wrapping the ReraiserThreadGroup running the call in
|
||||
parallel.
|
||||
Raises:
|
||||
AttributeError if the wrapped objects aren't callable.
|
||||
"""
|
||||
self.pGet(None)
|
||||
|
||||
if not self._objs:
|
||||
raise AttributeError('Nothing to call.')
|
||||
for o in self._objs:
|
||||
if not callable(o):
|
||||
raise AttributeError("'%s' is not callable" % o.__name__)
|
||||
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = reraiser_thread.ReraiserThreadGroup(
|
||||
[reraiser_thread.ReraiserThread(
|
||||
o, args=args, kwargs=kwargs,
|
||||
name='%s.%s' % (str(d), o.__name__))
|
||||
for d, o in zip(self._orig_objs, self._objs)])
|
||||
r._objs.StartAll() # pylint: disable=W0212
|
||||
return r
|
||||
|
||||
def pFinish(self, timeout):
|
||||
"""Finish any outstanding asynchronous operations.
|
||||
|
||||
Args:
|
||||
timeout: The maximum number of seconds to wait for an individual
|
||||
result to return, or None to wait forever.
|
||||
Returns:
|
||||
self, now emulating the return values.
|
||||
"""
|
||||
self._assertNoShadow('pFinish')
|
||||
if isinstance(self._objs, reraiser_thread.ReraiserThreadGroup):
|
||||
self._objs.JoinAll()
|
||||
self._objs = self._objs.GetAllReturnValues(
|
||||
watchdog_timer.WatchdogTimer(timeout))
|
||||
return self
|
||||
|
||||
def pGet(self, timeout):
|
||||
"""Get the current wrapped objects.
|
||||
|
||||
Args:
|
||||
timeout: Same as |pFinish|.
|
||||
Returns:
|
||||
A list of the results, in order of the provided devices.
|
||||
Raises:
|
||||
Any exception raised by any of the called functions.
|
||||
"""
|
||||
self._assertNoShadow('pGet')
|
||||
self.pFinish(timeout)
|
||||
return self._objs
|
||||
|
||||
def pMap(self, f, *args, **kwargs):
|
||||
"""Map a function across the current wrapped objects in parallel.
|
||||
|
||||
This calls f(o, *args, **kwargs) for each o in the set of wrapped objects.
|
||||
|
||||
Note that this call is asynchronous. Call pFinish on the return value to
|
||||
block until the call finishes.
|
||||
|
||||
Args:
|
||||
f: The function to call.
|
||||
args: The positional args to pass to f.
|
||||
kwargs: The keyword args to pass to f.
|
||||
Returns:
|
||||
A Parallelizer wrapping the ReraiserThreadGroup running the map in
|
||||
parallel.
|
||||
"""
|
||||
self._assertNoShadow('pMap')
|
||||
r = type(self)(self._orig_objs)
|
||||
r._objs = reraiser_thread.ReraiserThreadGroup(
|
||||
[reraiser_thread.ReraiserThread(
|
||||
f, args=tuple([o] + list(args)), kwargs=kwargs,
|
||||
name='%s(%s)' % (f.__name__, d))
|
||||
for d, o in zip(self._orig_objs, self._objs)])
|
||||
r._objs.StartAll() # pylint: disable=W0212
|
||||
return r
|
||||
|
||||
def _assertNoShadow(self, attr_name):
|
||||
"""Ensures that |attr_name| isn't shadowing part of the wrapped obejcts.
|
||||
|
||||
If the wrapped objects _do_ have an |attr_name| attribute, it will be
|
||||
inaccessible to clients.
|
||||
|
||||
Args:
|
||||
attr_name: The attribute to check.
|
||||
Raises:
|
||||
AssertionError if the wrapped objects have an attribute named 'attr_name'
|
||||
or '_assertNoShadow'.
|
||||
"""
|
||||
if isinstance(self._objs, reraiser_thread.ReraiserThreadGroup):
|
||||
assert not hasattr(self._objs, '_assertNoShadow')
|
||||
assert not hasattr(self._objs, attr_name)
|
||||
else:
|
||||
assert not any(hasattr(o, '_assertNoShadow') for o in self._objs)
|
||||
assert not any(hasattr(o, attr_name) for o in self._objs)
|
||||
|
||||
|
||||
class SyncParallelizer(Parallelizer):
|
||||
"""A Parallelizer that blocks on function calls."""
|
||||
|
||||
#override
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Emulate calling |self| with |args| and |kwargs|.
|
||||
|
||||
Note that this call is synchronous.
|
||||
|
||||
Returns:
|
||||
A Parallelizer emulating the value returned from calling |self| with
|
||||
|args| and |kwargs|.
|
||||
Raises:
|
||||
AttributeError if the wrapped objects aren't callable.
|
||||
"""
|
||||
r = super(SyncParallelizer, self).__call__(*args, **kwargs)
|
||||
r.pFinish(None)
|
||||
return r
|
||||
|
||||
#override
|
||||
def pMap(self, f, *args, **kwargs):
|
||||
"""Map a function across the current wrapped objects in parallel.
|
||||
|
||||
This calls f(o, *args, **kwargs) for each o in the set of wrapped objects.
|
||||
|
||||
Note that this call is synchronous.
|
||||
|
||||
Args:
|
||||
f: The function to call.
|
||||
args: The positional args to pass to f.
|
||||
kwargs: The keyword args to pass to f.
|
||||
Returns:
|
||||
A Parallelizer wrapping the ReraiserThreadGroup running the map in
|
||||
parallel.
|
||||
"""
|
||||
r = super(SyncParallelizer, self).pMap(f, *args, **kwargs)
|
||||
r.pFinish(None)
|
||||
return r
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.utils.parallelizer import *
|
||||
|
|
|
@ -1,158 +1,8 @@
|
|||
# Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""Thread and ThreadGroup that reraise exceptions on the main thread."""
|
||||
# pylint: disable=W0212
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from pylib.utils import watchdog_timer
|
||||
|
||||
|
||||
class TimeoutError(Exception):
|
||||
"""Module-specific timeout exception."""
|
||||
pass
|
||||
|
||||
|
||||
def LogThreadStack(thread):
|
||||
"""Log the stack for the given thread.
|
||||
|
||||
Args:
|
||||
thread: a threading.Thread instance.
|
||||
"""
|
||||
stack = sys._current_frames()[thread.ident]
|
||||
logging.critical('*' * 80)
|
||||
logging.critical('Stack dump for thread %r', thread.name)
|
||||
logging.critical('*' * 80)
|
||||
for filename, lineno, name, line in traceback.extract_stack(stack):
|
||||
logging.critical('File: "%s", line %d, in %s', filename, lineno, name)
|
||||
if line:
|
||||
logging.critical(' %s', line.strip())
|
||||
logging.critical('*' * 80)
|
||||
|
||||
|
||||
class ReraiserThread(threading.Thread):
|
||||
"""Thread class that can reraise exceptions."""
|
||||
|
||||
def __init__(self, func, args=None, kwargs=None, name=None):
|
||||
"""Initialize thread.
|
||||
|
||||
Args:
|
||||
func: callable to call on a new thread.
|
||||
args: list of positional arguments for callable, defaults to empty.
|
||||
kwargs: dictionary of keyword arguments for callable, defaults to empty.
|
||||
name: thread name, defaults to Thread-N.
|
||||
"""
|
||||
super(ReraiserThread, self).__init__(name=name)
|
||||
if not args:
|
||||
args = []
|
||||
if not kwargs:
|
||||
kwargs = {}
|
||||
self.daemon = True
|
||||
self._func = func
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._ret = None
|
||||
self._exc_info = None
|
||||
|
||||
def ReraiseIfException(self):
|
||||
"""Reraise exception if an exception was raised in the thread."""
|
||||
if self._exc_info:
|
||||
raise self._exc_info[0], self._exc_info[1], self._exc_info[2]
|
||||
|
||||
def GetReturnValue(self):
|
||||
"""Reraise exception if present, otherwise get the return value."""
|
||||
self.ReraiseIfException()
|
||||
return self._ret
|
||||
|
||||
#override
|
||||
def run(self):
|
||||
"""Overrides Thread.run() to add support for reraising exceptions."""
|
||||
try:
|
||||
self._ret = self._func(*self._args, **self._kwargs)
|
||||
except: # pylint: disable=W0702
|
||||
self._exc_info = sys.exc_info()
|
||||
|
||||
|
||||
class ReraiserThreadGroup(object):
|
||||
"""A group of ReraiserThread objects."""
|
||||
|
||||
def __init__(self, threads=None):
|
||||
"""Initialize thread group.
|
||||
|
||||
Args:
|
||||
threads: a list of ReraiserThread objects; defaults to empty.
|
||||
"""
|
||||
if not threads:
|
||||
threads = []
|
||||
self._threads = threads
|
||||
|
||||
def Add(self, thread):
|
||||
"""Add a thread to the group.
|
||||
|
||||
Args:
|
||||
thread: a ReraiserThread object.
|
||||
"""
|
||||
self._threads.append(thread)
|
||||
|
||||
def StartAll(self):
|
||||
"""Start all threads."""
|
||||
for thread in self._threads:
|
||||
thread.start()
|
||||
|
||||
def _JoinAll(self, watcher=None):
|
||||
"""Join all threads without stack dumps.
|
||||
|
||||
Reraises exceptions raised by the child threads and supports breaking
|
||||
immediately on exceptions raised on the main thread.
|
||||
|
||||
Args:
|
||||
watcher: Watchdog object providing timeout, by default waits forever.
|
||||
"""
|
||||
if watcher is None:
|
||||
watcher = watchdog_timer.WatchdogTimer(None)
|
||||
alive_threads = self._threads[:]
|
||||
while alive_threads:
|
||||
for thread in alive_threads[:]:
|
||||
if watcher.IsTimedOut():
|
||||
raise TimeoutError('Timed out waiting for %d of %d threads.' %
|
||||
(len(alive_threads), len(self._threads)))
|
||||
# Allow the main thread to periodically check for interrupts.
|
||||
thread.join(0.1)
|
||||
if not thread.isAlive():
|
||||
alive_threads.remove(thread)
|
||||
# All threads are allowed to complete before reraising exceptions.
|
||||
for thread in self._threads:
|
||||
thread.ReraiseIfException()
|
||||
|
||||
def JoinAll(self, watcher=None):
|
||||
"""Join all threads.
|
||||
|
||||
Reraises exceptions raised by the child threads and supports breaking
|
||||
immediately on exceptions raised on the main thread. Unfinished threads'
|
||||
stacks will be logged on watchdog timeout.
|
||||
|
||||
Args:
|
||||
watcher: Watchdog object providing timeout, by default waits forever.
|
||||
"""
|
||||
try:
|
||||
self._JoinAll(watcher)
|
||||
except TimeoutError:
|
||||
for thread in (t for t in self._threads if t.isAlive()):
|
||||
LogThreadStack(thread)
|
||||
raise
|
||||
|
||||
def GetAllReturnValues(self, watcher=None):
|
||||
"""Get all return values, joining all threads if necessary.
|
||||
|
||||
Args:
|
||||
watcher: same as in |JoinAll|. Only used if threads are alive.
|
||||
"""
|
||||
if any([t.isAlive() for t in self._threads]):
|
||||
self.JoinAll(watcher)
|
||||
return [t.GetReturnValue() for t in self._threads]
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.utils.reraiser_thread import *
|
||||
|
|
|
@ -1,44 +1,8 @@
|
|||
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""Helper functions common to native, java and host-driven test runners."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
"""Custom log formatter."""
|
||||
|
||||
#override
|
||||
def __init__(self, fmt='%(threadName)-4s %(message)s'):
|
||||
# Can't use super() because in older Python versions logging.Formatter does
|
||||
# not inherit from object.
|
||||
logging.Formatter.__init__(self, fmt=fmt)
|
||||
self._creation_time = time.time()
|
||||
|
||||
#override
|
||||
def format(self, record):
|
||||
# Can't use super() because in older Python versions logging.Formatter does
|
||||
# not inherit from object.
|
||||
msg = logging.Formatter.format(self, record)
|
||||
if 'MainThread' in msg[:19]:
|
||||
msg = msg.replace('MainThread', 'Main', 1)
|
||||
timediff = time.time() - self._creation_time
|
||||
return '%s %8.3fs %s' % (record.levelname[0], timediff, msg)
|
||||
|
||||
|
||||
def SetLogLevel(verbose_count):
|
||||
"""Sets log level as |verbose_count|."""
|
||||
log_level = logging.WARNING # Default.
|
||||
if verbose_count == 1:
|
||||
log_level = logging.INFO
|
||||
elif verbose_count >= 2:
|
||||
log_level = logging.DEBUG
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(log_level)
|
||||
custom_handler = logging.StreamHandler(sys.stdout)
|
||||
custom_handler.setFormatter(CustomFormatter())
|
||||
logging.getLogger().addHandler(custom_handler)
|
||||
from devil.utils.run_tests_helper import *
|
||||
|
|
|
@ -1,167 +1,8 @@
|
|||
# Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""A utility to run functions with timeouts and retries."""
|
||||
# pylint: disable=W0702
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from pylib.utils import reraiser_thread
|
||||
from pylib.utils import watchdog_timer
|
||||
|
||||
|
||||
class TimeoutRetryThread(reraiser_thread.ReraiserThread):
|
||||
def __init__(self, func, timeout, name):
|
||||
super(TimeoutRetryThread, self).__init__(func, name=name)
|
||||
self._watcher = watchdog_timer.WatchdogTimer(timeout)
|
||||
self._expired = False
|
||||
|
||||
def GetWatcher(self):
|
||||
"""Returns the watchdog keeping track of this thread's time."""
|
||||
return self._watcher
|
||||
|
||||
def GetElapsedTime(self):
|
||||
return self._watcher.GetElapsed()
|
||||
|
||||
def GetRemainingTime(self, required=0, msg=None):
|
||||
"""Get the remaining time before the thread times out.
|
||||
|
||||
Useful to send as the |timeout| parameter of async IO operations.
|
||||
|
||||
Args:
|
||||
required: minimum amount of time that will be required to complete, e.g.,
|
||||
some sleep or IO operation.
|
||||
msg: error message to show if timing out.
|
||||
|
||||
Returns:
|
||||
The number of seconds remaining before the thread times out, or None
|
||||
if the thread never times out.
|
||||
|
||||
Raises:
|
||||
reraiser_thread.TimeoutError if the remaining time is less than the
|
||||
required time.
|
||||
"""
|
||||
remaining = self._watcher.GetRemaining()
|
||||
if remaining is not None and remaining < required:
|
||||
if msg is None:
|
||||
msg = 'Timeout expired'
|
||||
if remaining > 0:
|
||||
msg += (', wait of %.1f secs required but only %.1f secs left'
|
||||
% (required, remaining))
|
||||
self._expired = True
|
||||
raise reraiser_thread.TimeoutError(msg)
|
||||
return remaining
|
||||
|
||||
def LogTimeoutException(self):
|
||||
"""Log the exception that terminated this thread."""
|
||||
if not self._expired:
|
||||
return
|
||||
logging.critical('*' * 80)
|
||||
logging.critical('%s on thread %r', self._exc_info[0].__name__, self.name)
|
||||
logging.critical('*' * 80)
|
||||
fmt_exc = ''.join(traceback.format_exception(*self._exc_info))
|
||||
for line in fmt_exc.splitlines():
|
||||
logging.critical(line.rstrip())
|
||||
logging.critical('*' * 80)
|
||||
|
||||
|
||||
def CurrentTimeoutThread():
|
||||
"""Get the current thread if it is a TimeoutRetryThread.
|
||||
|
||||
Returns:
|
||||
The current thread if it is a TimeoutRetryThread, otherwise None.
|
||||
"""
|
||||
current_thread = threading.current_thread()
|
||||
if isinstance(current_thread, TimeoutRetryThread):
|
||||
return current_thread
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def WaitFor(condition, wait_period=5, max_tries=None):
|
||||
"""Wait for a condition to become true.
|
||||
|
||||
Repeadly call the function condition(), with no arguments, until it returns
|
||||
a true value.
|
||||
|
||||
If called within a TimeoutRetryThread, it cooperates nicely with it.
|
||||
|
||||
Args:
|
||||
condition: function with the condition to check
|
||||
wait_period: number of seconds to wait before retrying to check the
|
||||
condition
|
||||
max_tries: maximum number of checks to make, the default tries forever
|
||||
or until the TimeoutRetryThread expires.
|
||||
|
||||
Returns:
|
||||
The true value returned by the condition, or None if the condition was
|
||||
not met after max_tries.
|
||||
|
||||
Raises:
|
||||
reraiser_thread.TimeoutError if the current thread is a TimeoutRetryThread
|
||||
and the timeout expires.
|
||||
"""
|
||||
condition_name = condition.__name__
|
||||
timeout_thread = CurrentTimeoutThread()
|
||||
while max_tries is None or max_tries > 0:
|
||||
result = condition()
|
||||
if max_tries is not None:
|
||||
max_tries -= 1
|
||||
msg = ['condition', repr(condition_name), 'met' if result else 'not met']
|
||||
if timeout_thread:
|
||||
msg.append('(%.1fs)' % timeout_thread.GetElapsedTime())
|
||||
logging.info(' '.join(msg))
|
||||
if result:
|
||||
return result
|
||||
if timeout_thread:
|
||||
timeout_thread.GetRemainingTime(wait_period,
|
||||
msg='Timed out waiting for %r' % condition_name)
|
||||
time.sleep(wait_period)
|
||||
return None
|
||||
|
||||
|
||||
def Run(func, timeout, retries, args=None, kwargs=None):
|
||||
"""Runs the passed function in a separate thread with timeouts and retries.
|
||||
|
||||
Args:
|
||||
func: the function to be wrapped.
|
||||
timeout: the timeout in seconds for each try.
|
||||
retries: the number of retries.
|
||||
args: list of positional args to pass to |func|.
|
||||
kwargs: dictionary of keyword args to pass to |func|.
|
||||
|
||||
Returns:
|
||||
The return value of func(*args, **kwargs).
|
||||
"""
|
||||
if not args:
|
||||
args = []
|
||||
if not kwargs:
|
||||
kwargs = {}
|
||||
|
||||
# The return value uses a list because Python variables are references, not
|
||||
# values. Closures make a copy of the reference, so updating the closure's
|
||||
# reference wouldn't update where the original reference pointed.
|
||||
ret = [None]
|
||||
def RunOnTimeoutThread():
|
||||
ret[0] = func(*args, **kwargs)
|
||||
|
||||
num_try = 1
|
||||
while True:
|
||||
child_thread = TimeoutRetryThread(
|
||||
RunOnTimeoutThread, timeout,
|
||||
name='TimeoutThread-%d-for-%s' % (num_try,
|
||||
threading.current_thread().name))
|
||||
try:
|
||||
thread_group = reraiser_thread.ReraiserThreadGroup([child_thread])
|
||||
thread_group.StartAll()
|
||||
thread_group.JoinAll(child_thread.GetWatcher())
|
||||
return ret[0]
|
||||
except:
|
||||
child_thread.LogTimeoutException()
|
||||
if num_try > retries:
|
||||
raise
|
||||
num_try += 1
|
||||
from devil.utils.timeout_retry import *
|
||||
|
|
|
@ -1,47 +1,8 @@
|
|||
# Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
# Copyright 2015 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.
|
||||
|
||||
"""WatchdogTimer timeout objects."""
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class WatchdogTimer(object):
|
||||
"""A resetable timeout-based watchdog.
|
||||
|
||||
This object is threadsafe.
|
||||
"""
|
||||
|
||||
def __init__(self, timeout):
|
||||
"""Initializes the watchdog.
|
||||
|
||||
Args:
|
||||
timeout: The timeout in seconds. If timeout is None it will never timeout.
|
||||
"""
|
||||
self._start_time = time.time()
|
||||
self._timeout = timeout
|
||||
|
||||
def Reset(self):
|
||||
"""Resets the timeout countdown."""
|
||||
self._start_time = time.time()
|
||||
|
||||
def GetElapsed(self):
|
||||
"""Returns the elapsed time of the watchdog."""
|
||||
return time.time() - self._start_time
|
||||
|
||||
def GetRemaining(self):
|
||||
"""Returns the remaining time of the watchdog."""
|
||||
if self._timeout:
|
||||
return self._timeout - self.GetElapsed()
|
||||
else:
|
||||
return None
|
||||
|
||||
def IsTimedOut(self):
|
||||
"""Whether the watchdog has timed out.
|
||||
|
||||
Returns:
|
||||
True if the watchdog has timed out, False otherwise.
|
||||
"""
|
||||
remaining = self.GetRemaining()
|
||||
return remaining is not None and remaining < 0
|
||||
from devil.utils.watchdog_timer import *
|
||||
|
|
|
@ -2,30 +2,7 @@
|
|||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
|
||||
def WriteToZipFile(zip_file, path, arc_path):
|
||||
"""Recursively write |path| to |zip_file| as |arc_path|.
|
||||
|
||||
zip_file: An open instance of zipfile.ZipFile.
|
||||
path: An absolute path to the file or directory to be zipped.
|
||||
arc_path: A relative path within the zip file to which the file or directory
|
||||
located at |path| should be written.
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
for dir_path, _, file_names in os.walk(path):
|
||||
dir_arc_path = os.path.join(arc_path, os.path.relpath(dir_path, path))
|
||||
logging.debug('dir: %s -> %s', dir_path, dir_arc_path)
|
||||
zip_file.write(dir_path, dir_arc_path, zipfile.ZIP_STORED)
|
||||
for f in file_names:
|
||||
file_path = os.path.join(dir_path, f)
|
||||
file_arc_path = os.path.join(dir_arc_path, f)
|
||||
logging.debug('file: %s -> %s', file_path, file_arc_path)
|
||||
zip_file.write(file_path, file_arc_path, zipfile.ZIP_DEFLATED)
|
||||
else:
|
||||
logging.debug('file: %s -> %s', path, arc_path)
|
||||
zip_file.write(path, arc_path, zipfile.ZIP_DEFLATED)
|
||||
# pylint: disable=unused-wildcard-import
|
||||
# pylint: disable=wildcard-import
|
||||
|
||||
from devil.utils.zip_utils import *
|
||||
|
|
Загрузка…
Ссылка в новой задаче