[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:
jbudorick 2015-09-01 18:00:30 -07:00 коммит произвёл Commit bot
Родитель c3e27fd223
Коммит 295f4dabaf
74 изменённых файлов: 6072 добавлений и 5830 удалений

Просмотреть файл

@ -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

4
android/devil/OWNERS Normal file
Просмотреть файл

@ -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 *