This commit is contained in:
2021-04-19 20:16:55 +02:00
commit a0ff94dca2
839 changed files with 198976 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
# 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
logging.getLogger('devil').addHandler(logging.NullHandler())

View File

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

View File

@@ -0,0 +1,384 @@
# 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 re
import xml.etree.ElementTree
import zipfile
from devil import base_error
from devil.android.ndk import abis
from devil.android.sdk import aapt
from devil.utils import cmd_helper
_MANIFEST_ATTRIBUTE_RE = re.compile(
r'\s*A: ([^\(\)= ]*)(?:\([^\(\)= ]*\))?='
r'(?:"(.*)" \(Raw: .*\)|\(type.*?\)(.*))$')
_MANIFEST_ELEMENT_RE = re.compile(r'\s*(?:E|N): (\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 ToHelper(path_or_helper):
"""Creates an ApkHelper unless one is already given."""
if isinstance(path_or_helper, basestring):
return ApkHelper(path_or_helper)
return path_or_helper
# To parse the manifest, the function uses a node stack where at each level of
# the stack it keeps the currently in focus node at that level (of indentation
# in the xmltree output, ie. depth in the tree). The height of the stack is
# determinded by line indentation. When indentation is increased so is the stack
# (by pushing a new empty node on to the stack). When indentation is decreased
# the top of the stack is popped (sometimes multiple times, until indentation
# matches the height of the stack). Each line parsed (either an attribute or an
# element) is added to the node at the top of the stack (after the stack has
# been popped/pushed due to indentation).
def _ParseManifestFromApk(apk):
aapt_output = aapt.Dump('xmltree', apk.path, 'AndroidManifest.xml')
parsed_manifest = {}
node_stack = [parsed_manifest]
indent = ' '
if aapt_output[0].startswith('N'):
# if the first line is a namespace then the root manifest is indented, and
# we need to add a dummy namespace node, then skip the first line (we dont
# care about namespaces).
node_stack.insert(0, {})
output_to_parse = aapt_output[1:]
else:
output_to_parse = aapt_output
for line in output_to_parse:
if len(line) == 0:
continue
# If namespaces are stripped, aapt still outputs the full url to the
# namespace and appends it to the attribute names.
line = line.replace('http://schemas.android.com/apk/res/android:', 'android:')
indent_depth = 0
while line[(len(indent) * indent_depth):].startswith(indent):
indent_depth += 1
# Pop the stack until the height of the stack is the same is the depth of
# the current line within the tree.
node_stack = node_stack[:indent_depth + 1]
node = node_stack[-1]
# Element nodes are a list of python dicts while attributes are just a dict.
# This is because multiple elements, at the same depth of tree and the same
# name, are all added to the same list keyed under the element name.
m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:])
if m:
manifest_key = m.group(1)
if manifest_key in node:
node[manifest_key] += [{}]
else:
node[manifest_key] = [{}]
node_stack += [node[manifest_key][-1]]
continue
m = _MANIFEST_ATTRIBUTE_RE.match(line[len(indent) * indent_depth:])
if m:
manifest_key = m.group(1)
if manifest_key in node:
raise base_error.BaseError(
"A single attribute should have one key and one value: {}"
.format(line))
else:
node[manifest_key] = m.group(2) or m.group(3)
continue
return parsed_manifest
def _ParseManifestFromBundle(bundle):
cmd = [bundle.path, 'dump-manifest']
status, stdout, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
if status != 0:
raise Exception('Failed running {} with output\n{}\n{}'.format(
' '.join(cmd), stdout, stderr))
return ParseManifestFromXml(stdout)
def ParseManifestFromXml(xml_str):
"""Parse an android bundle manifest.
As ParseManifestFromAapt, but uses the xml output from bundletool. Each
element is a dict, mapping attribute or children by name. Attributes map to
a dict (as they are unique), children map to a list of dicts (as there may
be multiple children with the same name).
Args:
xml_str (str) An xml string that is an android manifest.
Returns:
A dict holding the parsed manifest, as with ParseManifestFromAapt.
"""
root = xml.etree.ElementTree.fromstring(xml_str)
return {root.tag: [_ParseManifestXMLNode(root)]}
def _ParseManifestXMLNode(node):
out = {}
for name, value in node.attrib.items():
cleaned_name = name.replace(
'{http://schemas.android.com/apk/res/android}',
'android:').replace(
'{http://schemas.android.com/tools}',
'tools:')
out[cleaned_name] = value
for child in node:
out.setdefault(child.tag, []).append(_ParseManifestXMLNode(child))
return out
def _ParseNumericKey(obj, key, default=0):
val = obj.get(key)
if val is None:
return default
return int(val, 0)
class _ExportedActivity(object):
def __init__(self, name):
self.name = name
self.actions = set()
self.categories = set()
self.schemes = set()
def _IterateExportedActivities(manifest_info):
app_node = manifest_info['manifest'][0]['application'][0]
activities = app_node.get('activity', []) + app_node.get('activity-alias', [])
for activity_node in activities:
# Presence of intent filters make an activity exported by default.
has_intent_filter = 'intent-filter' in activity_node
if not _ParseNumericKey(
activity_node, 'android:exported', default=has_intent_filter):
continue
activity = _ExportedActivity(activity_node.get('android:name'))
# Merge all intent-filters into a single set because there is not
# currently a need to keep them separate.
for intent_filter in activity_node.get('intent-filter', []):
for action in intent_filter.get('action', []):
activity.actions.add(action.get('android:name'))
for category in intent_filter.get('category', []):
activity.categories.add(category.get('android:name'))
for data in intent_filter.get('data', []):
activity.schemes.add(data.get('android:scheme'))
yield activity
class ApkHelper(object):
def __init__(self, path):
self._apk_path = path
self._manifest = None
@property
def path(self):
return self._apk_path
@property
def is_bundle(self):
return self._apk_path.endswith('_bundle')
def GetActivityName(self):
"""Returns the name of the first launcher Activity in the apk."""
manifest_info = self._GetManifest()
for activity in _IterateExportedActivities(manifest_info):
if ('android.intent.action.MAIN' in activity.actions and
'android.intent.category.LAUNCHER' in activity.categories):
return self._ResolveName(activity.name)
return None
def GetViewActivityName(self):
"""Returns name of the first action=View Activity that can handle http."""
manifest_info = self._GetManifest()
for activity in _IterateExportedActivities(manifest_info):
if ('android.intent.action.VIEW' in activity.actions and
'http' in activity.schemes):
return self._ResolveName(activity.name)
return None
def GetInstrumentationName(
self, default='android.test.InstrumentationTestRunner'):
"""Returns the name of the Instrumentation in the apk."""
all_instrumentations = self.GetAllInstrumentations(default=default)
if len(all_instrumentations) != 1:
raise base_error.BaseError(
'There is more than one instrumentation. Expected one.')
else:
return self._ResolveName(all_instrumentations[0]['android:name'])
def GetAllInstrumentations(
self, default='android.test.InstrumentationTestRunner'):
"""Returns a list of all Instrumentations in the apk."""
try:
return self._GetManifest()['manifest'][0]['instrumentation']
except KeyError:
return [{'android:name': default}]
def GetPackageName(self):
"""Returns the package name of the apk."""
manifest_info = self._GetManifest()
try:
return manifest_info['manifest'][0]['package']
except KeyError:
raise Exception('Failed to determine package name of %s' % self._apk_path)
def GetPermissions(self):
manifest_info = self._GetManifest()
try:
return [p['android:name'] for
p in manifest_info['manifest'][0]['uses-permission']]
except KeyError:
return []
def GetSplitName(self):
"""Returns the name of the split of the apk."""
manifest_info = self._GetManifest()
try:
return manifest_info['manifest'][0]['split']
except KeyError:
return None
def HasIsolatedProcesses(self):
"""Returns whether any services exist that use isolatedProcess=true."""
manifest_info = self._GetManifest()
try:
application = manifest_info['manifest'][0]['application'][0]
services = application['service']
return any(
_ParseNumericKey(s, 'android:isolatedProcess') for s in services)
except KeyError:
return False
def GetAllMetadata(self):
"""Returns a list meta-data tags as (name, value) tuples."""
manifest_info = self._GetManifest()
try:
application = manifest_info['manifest'][0]['application'][0]
metadata = application['meta-data']
return [(x.get('android:name'), x.get('android:value')) for x in metadata]
except KeyError:
return []
def GetVersionCode(self):
"""Returns the versionCode as an integer, or None if not available."""
manifest_info = self._GetManifest()
try:
version_code = manifest_info['manifest'][0]['android:versionCode']
return int(version_code, 16)
except KeyError:
return None
def GetVersionName(self):
"""Returns the versionName as a string."""
manifest_info = self._GetManifest()
try:
version_name = manifest_info['manifest'][0]['android:versionName']
return version_name
except KeyError:
return ''
def GetMinSdkVersion(self):
"""Returns the minSdkVersion as a string, or None if not available.
Note: this cannot always be cast to an integer."""
manifest_info = self._GetManifest()
try:
uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
min_sdk_version = uses_sdk['android:minSdkVersion']
try:
# The common case is for this to be an integer. Convert to decimal
# notation (rather than hexadecimal) for readability, but convert back
# to a string for type consistency with the general case.
return str(int(min_sdk_version, 16))
except ValueError:
# In general (ex. apps with minSdkVersion set to pre-release Android
# versions), minSdkVersion can be a string (usually, the OS codename
# letter). For simplicity, don't do any validation on the value.
return min_sdk_version
except KeyError:
return None
def GetTargetSdkVersion(self):
"""Returns the targetSdkVersion as a string, or None if not available.
Note: this cannot always be cast to an integer."""
manifest_info = self._GetManifest()
try:
uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
target_sdk_version = uses_sdk['android:targetSdkVersion']
try:
# The common case is for this to be an integer. Convert to decimal
# notation (rather than hexadecimal) for readability, but convert back
# to a string for type consistency with the general case.
return str(int(target_sdk_version, 16))
except ValueError:
# In general (ex. apps targeting pre-release Android versions),
# targetSdkVersion can be a string (usually, the OS codename letter).
# For simplicity, don't do any validation on the value.
return target_sdk_version
except KeyError:
return None
def _GetManifest(self):
if not self._manifest:
app = ToHelper(self._apk_path)
if app.is_bundle:
self._manifest = _ParseManifestFromBundle(app)
else:
self._manifest = _ParseManifestFromApk(app)
return self._manifest
def _ResolveName(self, name):
name = name.lstrip('.')
if '.' not in name:
return '%s.%s' % (self.GetPackageName(), name)
return name
def _ListApkPaths(self):
with zipfile.ZipFile(self._apk_path) as z:
return z.namelist()
def GetAbis(self):
"""Returns a list of ABIs in the apk (empty list if no native code)."""
# Use lib/* to determine the compatible ABIs.
libs = set()
for path in self._ListApkPaths():
path_tokens = path.split('/')
if len(path_tokens) >= 2 and path_tokens[0] == 'lib':
libs.add(path_tokens[1])
lib_to_abi = {
abis.ARM: [abis.ARM, abis.ARM_64],
abis.ARM_64: [abis.ARM_64],
abis.X86: [abis.X86, abis.X86_64],
abis.X86_64: [abis.X86_64]
}
try:
output = set()
for lib in libs:
for abi in lib_to_abi[lib]:
output.add(abi)
return sorted(output)
except KeyError:
raise base_error.BaseError('Unexpected ABI in lib/* folder.')

View File

@@ -0,0 +1,382 @@
#! /usr/bin/env python
# Copyright 2017 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 os
import unittest
from devil import base_error
from devil import devil_env
from devil.android import apk_helper
from devil.android.ndk import abis
from devil.utils import mock_calls
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
# pylint: disable=line-too-long
_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: android:versionCode(0x0101021b)=(type 0x10)0x166de1ea
A: android:versionName(0x0101021c)="75.0.3763.0" (Raw: "75.0.3763.0")
A: package="org.chromium.abc" (Raw: "org.chromium.abc")
A: split="random_split" (Raw: "random_split")
E: uses-sdk (line=2)
A: android:minSdkVersion(0x0101020c)=(type 0x10)0x15
A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
E: uses-permission (line=2)
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
E: uses-permission (line=3)
A: android:name(0x01010003)="android.permission.READ_EXTERNAL_STORAGE" (Raw: "android.permission.READ_EXTERNAL_STORAGE")
E: uses-permission (line=4)
A: android:name(0x01010003)="android.permission.ACCESS_FINE_LOCATION" (Raw: "android.permission.ACCESS_FINE_LOCATION")
E: application (line=5)
E: activity (line=6)
A: android:name(0x01010003)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName")
A: android:exported(0x01010010)=(type 0x12)0xffffffff
E: service (line=7)
A: android:name(0x01010001)="org.chromium.RandomService" (Raw: "org.chromium.RandomService")
A: android:isolatedProcess(0x01010888)=(type 0x12)0xffffffff
E: activity (line=173)
A: android:name(0x01010003)=".MainActivity" (Raw: ".MainActivity")
E: intent-filter (line=177)
E: action (line=178)
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
E: category (line=180)
A: android:name(0x01010003)="android.intent.category.DEFAULT" (Raw: "android.intent.category.DEFAULT")
E: category (line=181)
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
E: activity-alias (line=173)
A: android:name(0x01010003)="org.chromium.ViewActivity" (Raw: "org.chromium.ViewActivity")
A: android:targetActivity(0x01010202)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName")
E: intent-filter (line=191)
E: action (line=192)
A: android:name(0x01010003)="android.intent.action.VIEW" (Raw: "android.intent.action.VIEW")
E: data (line=198)
A: android:scheme(0x01010027)="http" (Raw: "http")
E: data (line=199)
A: android:scheme(0x01010027)="https" (Raw: "https")
E: meta-data (line=43)
A: android:name(0x01010003)="name1" (Raw: "name1")
A: android:value(0x01010024)="value1" (Raw: "value1")
E: meta-data (line=43)
A: android:name(0x01010003)="name2" (Raw: "name2")
A: android:value(0x01010024)="value2" (Raw: "value2")
E: instrumentation (line=8)
A: android:label(0x01010001)="abc" (Raw: "abc")
A: android:name(0x01010003)="org.chromium.RandomJUnit4TestRunner" (Raw: "org.chromium.RandomJUnit4TestRunner")
A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
A: junit4=(type 0x12)0xffffffff (Raw: "true")
E: instrumentation (line=9)
A: android:label(0x01010001)="abc" (Raw: "abc")
A: android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner")
A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
"""
_NO_ISOLATED_SERVICES = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: package="org.chromium.abc" (Raw: "org.chromium.abc")
E: application (line=5)
E: activity (line=6)
A: android:name(0x01010003)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName")
A: android:exported(0x01010010)=(type 0x12)0xffffffff
E: service (line=7)
A: android:name(0x01010001)="org.chromium.RandomService" (Raw: "org.chromium.RandomService")
"""
_NO_SERVICES = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: package="org.chromium.abc" (Raw: "org.chromium.abc")
E: application (line=5)
E: activity (line=6)
A: android:name(0x01010003)="org.chromium.ActivityName" (Raw: "org.chromium.ActivityName")
A: android:exported(0x01010010)=(type 0x12)0xffffffff
"""
_NO_APPLICATION = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: package="org.chromium.abc" (Raw: "org.chromium.abc")
"""
_SINGLE_INSTRUMENTATION_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
E: instrumentation (line=8)
A: android:label(0x01010001)="xyz" (Raw: "xyz")
A: android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner")
A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
"""
_SINGLE_J4_INSTRUMENTATION_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
E: instrumentation (line=8)
A: android:label(0x01010001)="xyz" (Raw: "xyz")
A: android:name(0x01010003)="org.chromium.RandomJ4TestRunner" (Raw: "org.chromium.RandomJ4TestRunner")
A: android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
A: junit4=(type 0x12)0xffffffff (Raw: "true")
"""
_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
E: uses-sdk (line=2)
A: android:minSdkVersion(0x0101020c)="Q" (Raw: "Q")
A: android:targetSdkVersion(0x01010270)="Q" (Raw: "Q")
"""
_NO_NAMESPACE_MANIFEST_DUMP = """E: manifest (line=1)
A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
E: instrumentation (line=8)
A: http://schemas.android.com/apk/res/android:label(0x01010001)="xyz" (Raw: "xyz")
A: http://schemas.android.com/apk/res/android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner")
A: http://schemas.android.com/apk/res/android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
"""
# pylint: enable=line-too-long
def _MockAaptDump(manifest_dump):
return mock.patch(
'devil.android.sdk.aapt.Dump',
mock.Mock(side_effect=None, return_value=manifest_dump.split('\n')))
def _MockListApkPaths(files):
return mock.patch(
'devil.android.apk_helper.ApkHelper._ListApkPaths',
mock.Mock(side_effect=None, return_value=files))
class ApkHelperTest(mock_calls.TestCase):
def testGetInstrumentationName(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
with self.assertRaises(base_error.BaseError):
helper.GetInstrumentationName()
def testGetActivityName(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals(
helper.GetActivityName(), 'org.chromium.abc.MainActivity')
def testGetViewActivityName(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals(
helper.GetViewActivityName(), 'org.chromium.ViewActivity')
def testGetAllInstrumentations(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
all_instrumentations = helper.GetAllInstrumentations()
self.assertEquals(len(all_instrumentations), 2)
self.assertEquals(all_instrumentations[0]['android:name'],
'org.chromium.RandomJUnit4TestRunner')
self.assertEquals(all_instrumentations[1]['android:name'],
'org.chromium.RandomTestRunner')
def testGetPackageName(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals(helper.GetPackageName(), 'org.chromium.abc')
def testGetPermssions(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
all_permissions = helper.GetPermissions()
self.assertEquals(len(all_permissions), 3)
self.assertTrue('android.permission.INTERNET' in all_permissions)
self.assertTrue(
'android.permission.READ_EXTERNAL_STORAGE' in all_permissions)
self.assertTrue(
'android.permission.ACCESS_FINE_LOCATION' in all_permissions)
def testGetSplitName(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals(helper.GetSplitName(), 'random_split')
def testHasIsolatedProcesses_noApplication(self):
with _MockAaptDump(_NO_APPLICATION):
helper = apk_helper.ApkHelper('')
self.assertFalse(helper.HasIsolatedProcesses())
def testHasIsolatedProcesses_noServices(self):
with _MockAaptDump(_NO_SERVICES):
helper = apk_helper.ApkHelper('')
self.assertFalse(helper.HasIsolatedProcesses())
def testHasIsolatedProcesses_oneNotIsolatedProcess(self):
with _MockAaptDump(_NO_ISOLATED_SERVICES):
helper = apk_helper.ApkHelper('')
self.assertFalse(helper.HasIsolatedProcesses())
def testHasIsolatedProcesses_oneIsolatedProcess(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertTrue(helper.HasIsolatedProcesses())
def testGetSingleInstrumentationName(self):
with _MockAaptDump(_SINGLE_INSTRUMENTATION_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('org.chromium.RandomTestRunner',
helper.GetInstrumentationName())
def testGetSingleJUnit4InstrumentationName(self):
with _MockAaptDump(_SINGLE_J4_INSTRUMENTATION_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('org.chromium.RandomJ4TestRunner',
helper.GetInstrumentationName())
def testGetAllMetadata(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals([('name1', 'value1'), ('name2', 'value2')],
helper.GetAllMetadata())
def testGetVersionCode(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals(376300010, helper.GetVersionCode())
def testGetVersionName(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('75.0.3763.0', helper.GetVersionName())
def testGetMinSdkVersion_integerValue(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('21', helper.GetMinSdkVersion())
def testGetMinSdkVersion_stringValue(self):
with _MockAaptDump(_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('Q', helper.GetMinSdkVersion())
def testGetTargetSdkVersion_integerValue(self):
with _MockAaptDump(_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('28', helper.GetTargetSdkVersion())
def testGetTargetSdkVersion_stringValue(self):
with _MockAaptDump(_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('Q', helper.GetTargetSdkVersion())
def testGetSingleInstrumentationName_strippedNamespaces(self):
with _MockAaptDump(_NO_NAMESPACE_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
self.assertEquals('org.chromium.RandomTestRunner',
helper.GetInstrumentationName())
def testGetArchitectures(self):
AbiPair = collections.namedtuple('AbiPair', ['abi32bit', 'abi64bit'])
for abi_pair in [AbiPair('lib/' + abis.ARM, 'lib/' + abis.ARM_64),
AbiPair('lib/' + abis.X86, 'lib/' + abis.X86_64)]:
with _MockListApkPaths([abi_pair.abi32bit]):
helper = apk_helper.ApkHelper('')
self.assertEquals(set([os.path.basename(abi_pair.abi32bit),
os.path.basename(abi_pair.abi64bit)]),
set(helper.GetAbis()))
with _MockListApkPaths([abi_pair.abi32bit, abi_pair.abi64bit]):
helper = apk_helper.ApkHelper('')
self.assertEquals(set([os.path.basename(abi_pair.abi32bit),
os.path.basename(abi_pair.abi64bit)]),
set(helper.GetAbis()))
with _MockListApkPaths([abi_pair.abi64bit]):
helper = apk_helper.ApkHelper('')
self.assertEquals(set([os.path.basename(abi_pair.abi64bit)]),
set(helper.GetAbis()))
def testParseXmlManifest(self):
self.assertEquals({
'manifest': [
{'android:compileSdkVersion': '28',
'android:versionCode': '2',
'uses-sdk': [
{'android:minSdkVersion': '24',
'android:targetSdkVersion': '28'}],
'uses-permission': [
{'android:name':
'android.permission.ACCESS_COARSE_LOCATION'},
{'android:name':
'android.permission.ACCESS_NETWORK_STATE'}],
'application': [
{'android:allowBackup': 'true',
'android:extractNativeLibs': 'false',
'android:fullBackupOnly': 'false',
'meta-data': [
{'android:name': 'android.allow_multiple',
'android:value': 'true'},
{'android:name': 'multiwindow',
'android:value': 'true'}],
'activity': [
{'android:configChanges': '0x00001fb3',
'android:excludeFromRecents': 'true',
'android:name': 'ChromeLauncherActivity',
'intent-filter': [
{'action': [
{'android:name': 'dummy.action'}],
'category': [
{'android:name': 'DAYDREAM'},
{'android:name': 'CARDBOARD'}]}]},
{'android:enabled': 'false',
'android:name': 'MediaLauncherActivity',
'intent-filter': [
{'tools:ignore': 'AppLinkUrlError',
'action': [{'android:name': 'VIEW'}],
'category': [{'android:name': 'DEFAULT'}],
'data': [
{'android:mimeType': 'audio/*'},
{'android:mimeType': 'image/*'},
{'android:mimeType': 'video/*'},
{'android:scheme': 'file'},
{'android:scheme': 'content'}]}]}]}]}]},
apk_helper.ParseManifestFromXml("""
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:compileSdkVersion="28" android:versionCode="2">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="28"/>
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application android:allowBackup="true"
android:extractNativeLibs="false"
android:fullBackupOnly="false">
<meta-data android:name="android.allow_multiple"
android:value="true"/>
<meta-data android:name="multiwindow"
android:value="true"/>
<activity android:configChanges="0x00001fb3"
android:excludeFromRecents="true"
android:name="ChromeLauncherActivity">
<intent-filter>
<action android:name="dummy.action"/>
<category android:name="DAYDREAM"/>
<category android:name="CARDBOARD"/>
</intent-filter>
</activity>
<activity android:enabled="false"
android:name="MediaLauncherActivity">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="VIEW"/>
<category android:name="DEFAULT"/>
<data android:mimeType="audio/*"/>
<data android:mimeType="image/*"/>
<data android:mimeType="video/*"/>
<data android:scheme="file"/>
<data android:scheme="content"/>
</intent-filter>
</activity>
</application>
</manifest>"""))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,243 @@
# 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 functionality to interact with UI elements of an Android app."""
import collections
import re
from xml.etree import ElementTree as element_tree
from devil.android import decorators
from devil.android import device_temp_file
from devil.utils import geometry
from devil.utils import timeout_retry
_DEFAULT_SHORT_TIMEOUT = 10
_DEFAULT_SHORT_RETRIES = 3
_DEFAULT_LONG_TIMEOUT = 30
_DEFAULT_LONG_RETRIES = 0
# Parse rectangle bounds given as: '[left,top][right,bottom]'.
_RE_BOUNDS = re.compile(
r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]')
class _UiNode(object):
def __init__(self, device, xml_node, package=None):
"""Object to interact with a UI node from an xml snapshot.
Note: there is usually no need to call this constructor directly. Instead,
use an AppUi object (below) to grab an xml screenshot from a device and
find nodes in it.
Args:
device: A device_utils.DeviceUtils instance.
xml_node: An ElementTree instance of the node to interact with.
package: An optional package name for the app owning this node.
"""
self._device = device
self._xml_node = xml_node
self._package = package
def _GetAttribute(self, key):
"""Get the value of an attribute of this node."""
return self._xml_node.attrib.get(key)
@property
def bounds(self):
"""Get a rectangle with the bounds of this UI node.
Returns:
A geometry.Rectangle instance.
"""
d = _RE_BOUNDS.match(self._GetAttribute('bounds')).groupdict()
return geometry.Rectangle.FromDict({k: int(v) for k, v in d.iteritems()})
def Tap(self, point=None, dp_units=False):
"""Send a tap event to the UI node.
Args:
point: An optional geometry.Point instance indicating the location to
tap, relative to the bounds of the UI node, i.e. (0, 0) taps the
top-left corner. If ommited, the center of the node is tapped.
dp_units: If True, indicates that the coordinates of the point are given
in device-independent pixels; otherwise they are assumed to be "real"
pixels. This option has no effect when the point is ommited.
"""
if point is None:
point = self.bounds.center
else:
if dp_units:
point = (float(self._device.pixel_density) / 160) * point
point += self.bounds.top_left
x, y = (str(int(v)) for v in point)
self._device.RunShellCommand(['input', 'tap', x, y], check_return=True)
def Dump(self):
"""Get a brief summary of the child nodes that can be found on this node.
Returns:
A list of lines that can be logged or otherwise printed.
"""
summary = collections.defaultdict(set)
for node in self._xml_node.iter():
package = node.get('package') or '(no package)'
label = node.get('resource-id') or '(no id)'
text = node.get('text')
if text:
label = '%s[%r]' % (label, text)
summary[package].add(label)
lines = []
for package, labels in sorted(summary.iteritems()):
lines.append('- %s:' % package)
for label in sorted(labels):
lines.append(' - %s' % label)
return lines
def __getitem__(self, key):
"""Retrieve a child of this node by its index.
Args:
key: An integer with the index of the child to retrieve.
Returns:
A UI node instance of the selected child.
Raises:
IndexError if the index is out of range.
"""
return type(self)(self._device, self._xml_node[key], package=self._package)
def _Find(self, **kwargs):
"""Find the first descendant node that matches a given criteria.
Note: clients would usually call AppUi.GetUiNode or AppUi.WaitForUiNode
instead.
For example:
app = app_ui.AppUi(device, package='org.my.app')
app.GetUiNode(resource_id='some_element', text='hello')
would retrieve the first matching node with both of the xml attributes:
resource-id='org.my.app:id/some_element'
text='hello'
As the example shows, if given and needed, the value of the resource_id key
is auto-completed with the package name specified in the AppUi constructor.
Args:
Arguments are specified as key-value pairs, where keys correnspond to
attribute names in xml nodes (replacing any '-' with '_' to make them
valid identifiers). At least one argument must be supplied, and arguments
with a None value are ignored.
Returns:
A UI node instance of the first descendant node that matches ALL the
given key-value criteria; or None if no such node is found.
Raises:
TypeError if no search arguments are provided.
"""
matches_criteria = self._NodeMatcher(kwargs)
for node in self._xml_node.iter():
if matches_criteria(node):
return type(self)(self._device, node, package=self._package)
return None
def _NodeMatcher(self, kwargs):
# Auto-complete resource-id's using the package name if available.
resource_id = kwargs.get('resource_id')
if (resource_id is not None
and self._package is not None
and ':id/' not in resource_id):
kwargs['resource_id'] = '%s:id/%s' % (self._package, resource_id)
criteria = [(k.replace('_', '-'), v)
for k, v in kwargs.iteritems()
if v is not None]
if not criteria:
raise TypeError('At least one search criteria should be specified')
return lambda node: all(node.get(k) == v for k, v in criteria)
class AppUi(object):
# timeout and retry arguments appear unused, but are handled by decorator.
# pylint: disable=unused-argument
def __init__(self, device, package=None):
"""Object to interact with the UI of an Android app.
Args:
device: A device_utils.DeviceUtils instance.
package: An optional package name for the app.
"""
self._device = device
self._package = package
@property
def package(self):
return self._package
@decorators.WithTimeoutAndRetriesDefaults(_DEFAULT_SHORT_TIMEOUT,
_DEFAULT_SHORT_RETRIES)
def _GetRootUiNode(self, timeout=None, retries=None):
"""Get a node pointing to the root of the UI nodes on screen.
Note: This is currently implemented via adb calls to uiatomator and it
is *slow*, ~2 secs per call. Do not rely on low-level implementation
details that may change in the future.
TODO(crbug.com/567217): Swap to a more efficient implementation.
Args:
timeout: A number of seconds to wait for the uiautomator dump.
retries: Number of times to retry if the adb command fails.
Returns:
A UI node instance pointing to the root of the xml screenshot.
"""
with device_temp_file.DeviceTempFile(self._device.adb) as dtemp:
self._device.RunShellCommand(['uiautomator', 'dump', dtemp.name],
check_return=True)
xml_node = element_tree.fromstring(
self._device.ReadFile(dtemp.name, force_pull=True))
return _UiNode(self._device, xml_node, package=self._package)
def ScreenDump(self):
"""Get a brief summary of the nodes that can be found on the screen.
Returns:
A list of lines that can be logged or otherwise printed.
"""
return self._GetRootUiNode().Dump()
def GetUiNode(self, **kwargs):
"""Get the first node found matching a specified criteria.
Args:
See _UiNode._Find.
Returns:
A UI node instance of the node if found, otherwise None.
"""
# pylint: disable=protected-access
return self._GetRootUiNode()._Find(**kwargs)
@decorators.WithTimeoutAndRetriesDefaults(_DEFAULT_LONG_TIMEOUT,
_DEFAULT_LONG_RETRIES)
def WaitForUiNode(self, timeout=None, retries=None, **kwargs):
"""Wait for a node matching a given criteria to appear on the screen.
Args:
timeout: A number of seconds to wait for the matching node to appear.
retries: Number of times to retry in case of adb command errors.
For other args, to specify the search criteria, see _UiNode._Find.
Returns:
The UI node instance found.
Raises:
device_errors.CommandTimeoutError if the node is not found before the
timeout.
"""
def node_found():
return self.GetUiNode(**kwargs)
return timeout_retry.WaitFor(node_found)

View File

@@ -0,0 +1,191 @@
# 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.
"""Unit tests for the app_ui module."""
import unittest
from xml.etree import ElementTree as element_tree
from devil import devil_env
from devil.android import app_ui
from devil.android import device_errors
from devil.utils import geometry
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
MOCK_XML_LOADING = '''
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node bounds="[0,50][1536,178]" content-desc="Loading"
resource-id="com.example.app:id/spinner"/>
</hierarchy>
'''.strip()
MOCK_XML_LOADED = '''
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node bounds="[0,50][1536,178]" content-desc=""
resource-id="com.example.app:id/toolbar">
<node bounds="[0,58][112,170]" content-desc="Open navigation drawer"/>
<node bounds="[121,50][1536,178]"
resource-id="com.example.app:id/actionbar_custom_view">
<node bounds="[121,50][1424,178]"
resource-id="com.example.app:id/actionbar_title" text="Primary"/>
<node bounds="[1424,50][1536,178]" content-desc="Search"
resource-id="com.example.app:id/actionbar_search_button"/>
</node>
</node>
<node bounds="[0,178][576,1952]" resource-id="com.example.app:id/drawer">
<node bounds="[0,178][144,1952]"
resource-id="com.example.app:id/mini_drawer">
<node bounds="[40,254][104,318]" resource-id="com.example.app:id/avatar"/>
<node bounds="[16,354][128,466]" content-desc="Primary"
resource-id="com.example.app:id/image_view"/>
<node bounds="[16,466][128,578]" content-desc="Social"
resource-id="com.example.app:id/image_view"/>
<node bounds="[16,578][128,690]" content-desc="Promotions"
resource-id="com.example.app:id/image_view"/>
</node>
</node>
</hierarchy>
'''.strip()
class UiAppTest(unittest.TestCase):
def setUp(self):
self.device = mock.Mock()
self.device.pixel_density = 320 # Each dp pixel is 2 real pixels.
self.app = app_ui.AppUi(self.device, package='com.example.app')
self._setMockXmlScreenshots([MOCK_XML_LOADED])
def _setMockXmlScreenshots(self, xml_docs):
"""Mock self.app._GetRootUiNode to load nodes from some test xml_docs.
Each time the method is called it will return a UI node for each string
given in |xml_docs|, or rise a time out error when the list is exhausted.
"""
# pylint: disable=protected-access
def get_mock_root_ui_node(value):
if isinstance(value, Exception):
raise value
return app_ui._UiNode(
self.device, element_tree.fromstring(value), self.app.package)
xml_docs.append(device_errors.CommandTimeoutError('Timed out!'))
self.app._GetRootUiNode = mock.Mock(
side_effect=(get_mock_root_ui_node(doc) for doc in xml_docs))
def assertNodeHasAttribs(self, node, attr):
# pylint: disable=protected-access
for key, value in attr.iteritems():
self.assertEquals(node._GetAttribute(key), value)
def assertTappedOnceAt(self, x, y):
self.device.RunShellCommand.assert_called_once_with(
['input', 'tap', str(x), str(y)], check_return=True)
def testFind_byText(self):
node = self.app.GetUiNode(text='Primary')
self.assertNodeHasAttribs(node, {
'text': 'Primary',
'content-desc': None,
'resource-id': 'com.example.app:id/actionbar_title',
})
self.assertEquals(node.bounds, geometry.Rectangle([121, 50], [1424, 178]))
def testFind_byContentDesc(self):
node = self.app.GetUiNode(content_desc='Social')
self.assertNodeHasAttribs(node, {
'text': None,
'content-desc': 'Social',
'resource-id': 'com.example.app:id/image_view',
})
self.assertEquals(node.bounds, geometry.Rectangle([16, 466], [128, 578]))
def testFind_byResourceId_autocompleted(self):
node = self.app.GetUiNode(resource_id='image_view')
self.assertNodeHasAttribs(node, {
'content-desc': 'Primary',
'resource-id': 'com.example.app:id/image_view',
})
def testFind_byResourceId_absolute(self):
node = self.app.GetUiNode(resource_id='com.example.app:id/image_view')
self.assertNodeHasAttribs(node, {
'content-desc': 'Primary',
'resource-id': 'com.example.app:id/image_view',
})
def testFind_byMultiple(self):
node = self.app.GetUiNode(resource_id='image_view',
content_desc='Promotions')
self.assertNodeHasAttribs(node, {
'content-desc': 'Promotions',
'resource-id': 'com.example.app:id/image_view',
})
self.assertEquals(node.bounds, geometry.Rectangle([16, 578], [128, 690]))
def testFind_notFound(self):
node = self.app.GetUiNode(resource_id='does_not_exist')
self.assertIsNone(node)
def testFind_noArgsGiven(self):
# Same exception given by Python for a function call with not enough args.
with self.assertRaises(TypeError):
self.app.GetUiNode()
def testGetChildren(self):
node = self.app.GetUiNode(resource_id='mini_drawer')
self.assertNodeHasAttribs(
node[0], {'resource-id': 'com.example.app:id/avatar'})
self.assertNodeHasAttribs(node[1], {'content-desc': 'Primary'})
self.assertNodeHasAttribs(node[2], {'content-desc': 'Social'})
self.assertNodeHasAttribs(node[3], {'content-desc': 'Promotions'})
with self.assertRaises(IndexError):
# pylint: disable=pointless-statement
node[4]
def testTap_center(self):
node = self.app.GetUiNode(content_desc='Open navigation drawer')
node.Tap()
self.assertTappedOnceAt(56, 114)
def testTap_topleft(self):
node = self.app.GetUiNode(content_desc='Open navigation drawer')
node.Tap(geometry.Point(0, 0))
self.assertTappedOnceAt(0, 58)
def testTap_withOffset(self):
node = self.app.GetUiNode(content_desc='Open navigation drawer')
node.Tap(geometry.Point(10, 20))
self.assertTappedOnceAt(10, 78)
def testTap_withOffsetInDp(self):
node = self.app.GetUiNode(content_desc='Open navigation drawer')
node.Tap(geometry.Point(10, 20), dp_units=True)
self.assertTappedOnceAt(20, 98)
def testTap_dpUnitsIgnored(self):
node = self.app.GetUiNode(content_desc='Open navigation drawer')
node.Tap(dp_units=True)
self.assertTappedOnceAt(56, 114) # Still taps at center.
@mock.patch('time.sleep', mock.Mock())
def testWaitForUiNode_found(self):
self._setMockXmlScreenshots(
[MOCK_XML_LOADING, MOCK_XML_LOADING, MOCK_XML_LOADED])
node = self.app.WaitForUiNode(resource_id='actionbar_title')
self.assertNodeHasAttribs(node, {'text': 'Primary'})
@mock.patch('time.sleep', mock.Mock())
def testWaitForUiNode_notFound(self):
self._setMockXmlScreenshots(
[MOCK_XML_LOADING, MOCK_XML_LOADING, MOCK_XML_LOADING])
with self.assertRaises(device_errors.CommandTimeoutError):
self.app.WaitForUiNode(resource_id='actionbar_title')

View File

@@ -0,0 +1,679 @@
# 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 crash_handler
from devil.android import decorators
from devil.android import device_errors
from devil.android import device_utils
from devil.android.sdk import version_codes
from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3
_DEVICE_PROFILES = [
{
'name': ['Nexus 4'],
'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.
'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'],
'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'],
'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'],
'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',
},
{
'name': ['Nexus 5X'],
'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': None,
'voltage': None,
'current': None,
},
{ # Galaxy s5
'name': ['SM-G900H'],
'enable_command': (
'chmod 644 /sys/class/power_supply/battery/test_mode && '
'chmod 644 /sys/class/power_supply/sec-charger/current_now && '
'echo 0 > /sys/class/power_supply/battery/test_mode && '
'echo 9999 > /sys/class/power_supply/sec-charger/current_now &&'
'dumpsys battery reset'),
'disable_command': (
'chmod 644 /sys/class/power_supply/battery/test_mode && '
'chmod 644 /sys/class/power_supply/sec-charger/current_now && '
'echo 1 > /sys/class/power_supply/battery/test_mode && '
'echo 0 > /sys/class/power_supply/sec-charger/current_now && '
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
'charge_counter': None,
'voltage': '/sys/class/power_supply/sec-fuelgauge/voltage_now',
'current': '/sys/class/power_supply/sec-charger/current_now',
},
{ # Galaxy s6, Galaxy s6, Galaxy s6 edge
'name': ['SM-G920F', 'SM-G920V', 'SM-G925V'],
'enable_command': (
'chmod 644 /sys/class/power_supply/battery/test_mode && '
'chmod 644 /sys/class/power_supply/max77843-charger/current_now && '
'echo 0 > /sys/class/power_supply/battery/test_mode && '
'echo 9999 > /sys/class/power_supply/max77843-charger/current_now &&'
'dumpsys battery reset'),
'disable_command': (
'chmod 644 /sys/class/power_supply/battery/test_mode && '
'chmod 644 /sys/class/power_supply/max77843-charger/current_now && '
'echo 1 > /sys/class/power_supply/battery/test_mode && '
'echo 0 > /sys/class/power_supply/max77843-charger/current_now && '
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
'charge_counter': None,
'voltage': '/sys/class/power_supply/max77843-fuelgauge/voltage_now',
'current': '/sys/class/power_supply/max77843-charger/current_now',
},
{ # Cherry Mobile One
'name': ['W6210 (4560MMX_b fingerprint)'],
'enable_command': (
'echo "0 0" > /proc/mtk_battery_cmd/current_cmd && '
'dumpsys battery reset'),
'disable_command': (
'echo "0 1" > /proc/mtk_battery_cmd/current_cmd && '
'dumpsys battery set ac 0 && dumpsys battery set usb 0'),
'charge_counter': None,
'voltage': None,
'current': None,
},
]
# 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
_MAX_CHARGE_ERROR = 20
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 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:
logger.warning('Dumpsys battery not receiving updates. '
'Run dumpsys battery reset if this is in error.')
elif ':' not in line:
logger.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.
"""
# Wrapper function so that we can use `RetryOnSystemCrash`.
def GetBatteryInfoHelper(device):
return self.GetBatteryInfo()
battery_info = crash_handler.RetryOnSystemCrash(
GetBatteryInfoHelper, self._device)
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
# 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 < 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:
logger.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:
logger.warning('Battery is too low or discharge amount requested is too '
'high. Cannot discharge phone %s percent.', percent)
return
self._HardwareSetCharging(False)
def device_discharged():
self._HardwareSetCharging(True)
current_level = int(self.GetBatteryInfo().get('level'))
logger.info('current battery level: %s', current_level)
if battery_level - current_level >= percent:
return True
self._HardwareSetCharging(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.
Raises:
device_errors.DeviceChargingError: If error while charging is detected.
"""
self.SetCharging(True)
charge_status = {
'charge_failure_count': 0,
'last_charge_value': 0
}
def device_charged():
battery_level = self.GetBatteryInfo().get('level')
if battery_level is None:
logger.warning('Unable to find current battery level.')
battery_level = 100
else:
logger.info('current battery level: %s', battery_level)
battery_level = int(battery_level)
# Use > so that it will not reset if charge is going down.
if battery_level > charge_status['last_charge_value']:
charge_status['last_charge_value'] = battery_level
charge_status['charge_failure_count'] = 0
else:
charge_status['charge_failure_count'] += 1
if (not battery_level >= level
and charge_status['charge_failure_count'] >= _MAX_CHARGE_ERROR):
raise device_errors.DeviceChargingError(
'Device not charging properly. Current level:%s Previous level:%s'
% (battery_level, charge_status['last_charge_value']))
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:
logger.warning('Unable to find current battery temperature.')
temp = 0
else:
logger.info('Current battery temperature: %s', temp)
if int(temp) <= target_temp:
return True
else:
if 'Nexus 5' in self._cache['profile']['name']:
self._DischargeDevice(1)
return False
self._DiscoverDeviceProfile()
self.EnableBatteryUpdates()
logger.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 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
"""
if self.GetCharging() == enabled:
logger.warning('Device charging already in expected state: %s', enabled)
return
self._DiscoverDeviceProfile()
if enabled:
if self._cache['profile']['enable_command']:
self._HardwareSetCharging(enabled)
else:
logger.info('Unable to enable charging via hardware. '
'Falling back to software enabling.')
self.EnableBatteryUpdates()
else:
if self._cache['profile']['enable_command']:
self._ClearPowerData()
self._HardwareSetCharging(enabled)
else:
logger.info('Unable to disable charging via hardware. '
'Falling back to software disabling.')
self.DisableBatteryUpdates()
def _HardwareSetCharging(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.')
command = (self._cache['profile']['enable_command'] if enabled
else self._cache['profile']['disable_command'])
def verify_charging():
return self.GetCharging() == enabled
self._device.RunShellCommand(
command, shell=True, check_return=True, as_root=True, large_output=True)
timeout_retry.WaitFor(verify_charging, wait_period=1)
@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.SetCharging(False, timeout=timeout, retries=retries)
yield
finally:
self.SetCharging(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 < version_codes.LOLLIPOP:
logger.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)
def test_if_clear():
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 float(l[_PWI_POWER_CONSUMPTION_INDEX]) != 0.0):
return False
return True
try:
timeout_retry.WaitFor(test_if_clear, wait_period=1)
return True
finally:
self._device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_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 in profile['name']:
self._cache['profile'] = profile
return True
self._cache['profile'] = {
'name': [],
'enable_command': None,
'disable_command': None,
'charge_counter': None,
'voltage': None,
'current': None,
}
return False

View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python
# 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.
"""
Unit tests for the contents of battery_utils.py
"""
# pylint: disable=protected-access,unused-argument
import logging
import unittest
from devil import devil_env
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
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
_DUMPSYS_OUTPUT = [
'9,0,i,uid,1000,test_package1',
'9,0,i,uid,1001,test_package2',
'9,1000,l,pwi,uid,1',
'9,1001,l,pwi,uid,2',
'9,0,l,pws,1728,2000,190,207',
]
class BatteryUtilsTest(mock_calls.TestCase):
_NEXUS_5 = {
'name': 'Nexus 5',
'witness_file': '/sys/kernel/debug/bq24192/INPUT_SRC_CONT',
'enable_command': (
'echo 0x4A > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
'echo 1 > /sys/class/power_supply/usb/online'),
'disable_command': (
'echo 0xCA > /sys/kernel/debug/bq24192/INPUT_SRC_CONT && '
'chmod 644 /sys/class/power_supply/usb/online && '
'echo 0 > /sys/class/power_supply/usb/online'),
'charge_counter': None,
'voltage': None,
'current': None,
}
_NEXUS_6 = {
'name': 'Nexus 6',
'witness_file': None,
'enable_command': None,
'disable_command': None,
'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',
}
_NEXUS_10 = {
'name': 'Nexus 10',
'witness_file': None,
'enable_command': None,
'disable_command': None,
'charge_counter': (
'/sys/class/power_supply/ds2784-fuelgauge/charge_counter_ext'),
'voltage': '/sys/class/power_supply/ds2784-fuelgauge/voltage_now',
'current': '/sys/class/power_supply/ds2784-fuelgauge/current_now',
}
def ShellError(self, output=None, status=1):
def action(cmd, *args, **kwargs):
raise device_errors.AdbShellCommandFailedError(
cmd, output, status, str(self.device))
if output is None:
output = 'Permission denied\n'
return action
def setUp(self):
self.adb = device_utils_test._AdbWrapperMock('0123456789abcdef')
self.device = device_utils.DeviceUtils(
self.adb, default_timeout=10, default_retries=0)
self.watchMethodCalls(self.call.adb, ignore=['GetDeviceSerial'])
self.battery = battery_utils.BatteryUtils(
self.device, default_timeout=10, default_retries=0)
class BatteryUtilsInitTest(unittest.TestCase):
def testInitWithDeviceUtil(self):
serial = '0fedcba987654321'
d = device_utils.DeviceUtils(serial)
b = battery_utils.BatteryUtils(d)
self.assertEqual(d, b._device)
def testInitWithMissing_fails(self):
with self.assertRaises(TypeError):
battery_utils.BatteryUtils(None)
with self.assertRaises(TypeError):
battery_utils.BatteryUtils('')
class BatteryUtilsSetChargingTest(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testHardwareSetCharging_enabled(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.device.RunShellCommand(
mock.ANY, shell=True, check_return=True, as_root=True,
large_output=True), []),
(self.call.battery.GetCharging(), False),
(self.call.battery.GetCharging(), True)):
self.battery._HardwareSetCharging(True)
def testHardwareSetCharging_alreadyEnabled(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.device.RunShellCommand(
mock.ANY, shell=True, check_return=True, as_root=True,
large_output=True), []),
(self.call.battery.GetCharging(), True)):
self.battery._HardwareSetCharging(True)
@mock.patch('time.sleep', mock.Mock())
def testHardwareSetCharging_disabled(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.device.RunShellCommand(
mock.ANY, shell=True, check_return=True, as_root=True,
large_output=True), []),
(self.call.battery.GetCharging(), True),
(self.call.battery.GetCharging(), False)):
self.battery._HardwareSetCharging(False)
class BatteryUtilsSetBatteryMeasurementTest(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testBatteryMeasurementWifi(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=22):
with self.assertCalls(
(self.call.battery._ClearPowerData(), True),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True),
[]),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), []),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True), [])):
with self.battery.BatteryMeasurement():
pass
@mock.patch('time.sleep', mock.Mock())
def testBatteryMeasurementUsb(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=22):
with self.assertCalls(
(self.call.battery._ClearPowerData(), True),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True),
[]),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), []),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
(self.call.battery.GetCharging(), True)):
with self.battery.BatteryMeasurement():
pass
class BatteryUtilsGetPowerData(BatteryUtilsTest):
def testGetPowerData(self):
with self.assertCalls(
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '-c'],
check_return=True, large_output=True),
_DUMPSYS_OUTPUT)):
data = self.battery.GetPowerData()
check = {
'system_total': 2000.0,
'per_package': {
'test_package1': {'uid': '1000', 'data': [1.0]},
'test_package2': {'uid': '1001', 'data': [2.0]}
}
}
self.assertEqual(data, check)
def testGetPowerData_packageCollisionSame(self):
self.battery._cache['uids'] = {'test_package1': '1000'}
with self.assertCall(
self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '-c'],
check_return=True, large_output=True),
_DUMPSYS_OUTPUT):
data = self.battery.GetPowerData()
check = {
'system_total': 2000.0,
'per_package': {
'test_package1': {'uid': '1000', 'data': [1.0]},
'test_package2': {'uid': '1001', 'data': [2.0]}
}
}
self.assertEqual(data, check)
def testGetPowerData_packageCollisionDifferent(self):
self.battery._cache['uids'] = {'test_package1': '1'}
with self.assertCall(
self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '-c'],
check_return=True, large_output=True),
_DUMPSYS_OUTPUT):
with self.assertRaises(device_errors.CommandFailedError):
self.battery.GetPowerData()
def testGetPowerData_cacheCleared(self):
with self.assertCalls(
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '-c'],
check_return=True, large_output=True),
_DUMPSYS_OUTPUT)):
self.battery._cache.clear()
data = self.battery.GetPowerData()
check = {
'system_total': 2000.0,
'per_package': {
'test_package1': {'uid': '1000', 'data': [1.0]},
'test_package2': {'uid': '1001', 'data': [2.0]}
}
}
self.assertEqual(data, check)
class BatteryUtilsChargeDevice(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testChargeDeviceToLevel_pass(self):
with self.assertCalls(
(self.call.battery.SetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '50'}),
(self.call.battery.GetBatteryInfo(), {'level': '100'})):
self.battery.ChargeDeviceToLevel(95)
@mock.patch('time.sleep', mock.Mock())
def testChargeDeviceToLevel_failureSame(self):
with self.assertCalls(
(self.call.battery.SetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '50'}),
(self.call.battery.GetBatteryInfo(), {'level': '50'}),
(self.call.battery.GetBatteryInfo(), {'level': '50'})):
with self.assertRaises(device_errors.DeviceChargingError):
old_max = battery_utils._MAX_CHARGE_ERROR
try:
battery_utils._MAX_CHARGE_ERROR = 2
self.battery.ChargeDeviceToLevel(95)
finally:
battery_utils._MAX_CHARGE_ERROR = old_max
@mock.patch('time.sleep', mock.Mock())
def testChargeDeviceToLevel_failureDischarge(self):
with self.assertCalls(
(self.call.battery.SetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '50'}),
(self.call.battery.GetBatteryInfo(), {'level': '49'}),
(self.call.battery.GetBatteryInfo(), {'level': '48'})):
with self.assertRaises(device_errors.DeviceChargingError):
old_max = battery_utils._MAX_CHARGE_ERROR
try:
battery_utils._MAX_CHARGE_ERROR = 2
self.battery.ChargeDeviceToLevel(95)
finally:
battery_utils._MAX_CHARGE_ERROR = old_max
class BatteryUtilsDischargeDevice(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testDischargeDevice_exact(self):
with self.assertCalls(
(self.call.battery.GetBatteryInfo(), {'level': '100'}),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery._HardwareSetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '99'})):
self.battery._DischargeDevice(1)
@mock.patch('time.sleep', mock.Mock())
def testDischargeDevice_over(self):
with self.assertCalls(
(self.call.battery.GetBatteryInfo(), {'level': '100'}),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery._HardwareSetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '50'})):
self.battery._DischargeDevice(1)
@mock.patch('time.sleep', mock.Mock())
def testDischargeDevice_takeslong(self):
with self.assertCalls(
(self.call.battery.GetBatteryInfo(), {'level': '100'}),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery._HardwareSetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '100'}),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery._HardwareSetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '99'}),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery._HardwareSetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '98'}),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery._HardwareSetCharging(True)),
(self.call.battery.GetBatteryInfo(), {'level': '97'})):
self.battery._DischargeDevice(3)
@mock.patch('time.sleep', mock.Mock())
def testDischargeDevice_dischargeTooClose(self):
with self.assertCalls(
(self.call.battery.GetBatteryInfo(), {'level': '100'})):
self.battery._DischargeDevice(99)
@mock.patch('time.sleep', mock.Mock())
def testDischargeDevice_percentageOutOfBounds(self):
with self.assertCalls(
(self.call.battery.GetBatteryInfo(), {'level': '100'})):
with self.assertRaises(ValueError):
self.battery._DischargeDevice(100)
with self.assertCalls(
(self.call.battery.GetBatteryInfo(), {'level': '100'})):
with self.assertRaises(ValueError):
self.battery._DischargeDevice(0)
class BatteryUtilsGetBatteryInfoTest(BatteryUtilsTest):
def testGetBatteryInfo_normal(self):
with self.assertCalls(
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True),
[
'Current Battery Service state:',
' AC powered: false',
' USB powered: true',
' level: 100',
' temperature: 321',
])):
self.assertEquals(
{
'AC powered': 'false',
'USB powered': 'true',
'level': '100',
'temperature': '321',
},
self.battery.GetBatteryInfo())
def testGetBatteryInfo_nothing(self):
with self.assertCalls(
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True), [])):
self.assertEquals({}, self.battery.GetBatteryInfo())
class BatteryUtilsGetChargingTest(BatteryUtilsTest):
def testGetCharging_usb(self):
with self.assertCall(
self.call.battery.GetBatteryInfo(), {'USB powered': 'true'}):
self.assertTrue(self.battery.GetCharging())
def testGetCharging_usbFalse(self):
with self.assertCall(
self.call.battery.GetBatteryInfo(), {'USB powered': 'false'}):
self.assertFalse(self.battery.GetCharging())
def testGetCharging_ac(self):
with self.assertCall(
self.call.battery.GetBatteryInfo(), {'AC powered': 'true'}):
self.assertTrue(self.battery.GetCharging())
def testGetCharging_wireless(self):
with self.assertCall(
self.call.battery.GetBatteryInfo(), {'Wireless powered': 'true'}):
self.assertTrue(self.battery.GetCharging())
def testGetCharging_unknown(self):
with self.assertCall(
self.call.battery.GetBatteryInfo(), {'level': '42'}):
self.assertFalse(self.battery.GetCharging())
class BatteryUtilsLetBatteryCoolToTemperatureTest(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_startUnder(self):
self.battery._cache['profile'] = self._NEXUS_6
with self.assertCalls(
(self.call.battery.EnableBatteryUpdates(), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '500'})):
self.battery.LetBatteryCoolToTemperature(600)
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_startOver(self):
self.battery._cache['profile'] = self._NEXUS_6
with self.assertCalls(
(self.call.battery.EnableBatteryUpdates(), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '500'}),
(self.call.battery.GetBatteryInfo(), {'temperature': '400'})):
self.battery.LetBatteryCoolToTemperature(400)
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_nexus5Hot(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.battery.EnableBatteryUpdates(), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '500'}),
(self.call.battery._DischargeDevice(1), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '400'})):
self.battery.LetBatteryCoolToTemperature(400)
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_nexus5Cool(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.battery.EnableBatteryUpdates(), []),
(self.call.battery.GetBatteryInfo(), {'temperature': '400'})):
self.battery.LetBatteryCoolToTemperature(400)
class BatteryUtilsSupportsFuelGaugeTest(BatteryUtilsTest):
def testSupportsFuelGauge_false(self):
self.battery._cache['profile'] = self._NEXUS_5
self.assertFalse(self.battery.SupportsFuelGauge())
def testSupportsFuelGauge_trueMax(self):
self.battery._cache['profile'] = self._NEXUS_6
# TODO(rnephew): Change this to assertTrue when we have support for
# disabling hardware charging on nexus 6.
self.assertFalse(self.battery.SupportsFuelGauge())
def testSupportsFuelGauge_trueDS(self):
self.battery._cache['profile'] = self._NEXUS_10
# TODO(rnephew): Change this to assertTrue when we have support for
# disabling hardware charging on nexus 10.
self.assertFalse(self.battery.SupportsFuelGauge())
class BatteryUtilsGetFuelGaugeChargeCounterTest(BatteryUtilsTest):
def testGetFuelGaugeChargeCounter_noFuelGauge(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertRaises(device_errors.CommandFailedError):
self.battery.GetFuelGaugeChargeCounter()
def testGetFuelGaugeChargeCounter_fuelGaugePresent(self):
self.battery._cache['profile'] = self._NEXUS_6
with self.assertCalls(
(self.call.battery.SupportsFuelGauge(), True),
(self.call.device.ReadFile(mock.ANY), '123')):
self.assertEqual(self.battery.GetFuelGaugeChargeCounter(), 123)
class BatteryUtilsSetCharging(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
def testSetCharging_softwareSetTrue(self):
self.battery._cache['profile'] = self._NEXUS_6
with self.assertCalls(
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), []),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
(self.call.battery.GetCharging(), True)):
self.battery.SetCharging(True)
@mock.patch('time.sleep', mock.Mock())
def testSetCharging_softwareSetFalse(self):
self.battery._cache['profile'] = self._NEXUS_6
with self.assertCalls(
(self.call.battery.GetCharging(), True),
(self.call.battery._ClearPowerData(), True),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
(self.call.battery.GetCharging(), False)):
self.battery.SetCharging(False)
@mock.patch('time.sleep', mock.Mock())
def testSetCharging_hardwareSetTrue(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.battery.GetCharging(), False),
(self.call.battery._HardwareSetCharging(True))):
self.battery.SetCharging(True)
@mock.patch('time.sleep', mock.Mock())
def testSetCharging_hardwareSetFalse(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.battery.GetCharging(), True),
(self.call.battery._ClearPowerData(), True),
(self.call.battery._HardwareSetCharging(False))):
self.battery.SetCharging(False)
def testSetCharging_expectedStateAlreadyTrue(self):
with self.assertCalls((self.call.battery.GetCharging(), True)):
self.battery.SetCharging(True)
def testSetCharging_expectedStateAlreadyFalse(self):
with self.assertCalls((self.call.battery.GetCharging(), False)):
self.battery.SetCharging(False)
class BatteryUtilsPowerMeasurement(BatteryUtilsTest):
def testPowerMeasurement_hardware(self):
self.battery._cache['profile'] = self._NEXUS_5
with self.assertCalls(
(self.call.battery.GetCharging(), True),
(self.call.battery._ClearPowerData(), True),
(self.call.battery._HardwareSetCharging(False)),
(self.call.battery.GetCharging(), False),
(self.call.battery._HardwareSetCharging(True))):
with self.battery.PowerMeasurement():
pass
@mock.patch('time.sleep', mock.Mock())
def testPowerMeasurement_software(self):
self.battery._cache['profile'] = self._NEXUS_6
with self.assertCalls(
(self.call.battery.GetCharging(), True),
(self.call.battery._ClearPowerData(), True),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '0'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'usb', '0'], check_return=True), []),
(self.call.battery.GetCharging(), False),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), []),
(self.call.battery.GetCharging(), False),
(self.call.device.RunShellCommand(
['dumpsys', 'battery'], check_return=True), ['UPDATES STOPPED']),
(self.call.battery.GetCharging(), True)):
with self.battery.PowerMeasurement():
pass
class BatteryUtilsDiscoverDeviceProfile(BatteryUtilsTest):
def testDiscoverDeviceProfile_known(self):
with self.patch_call(self.call.device.product_model,
return_value='Nexus 4'):
self.battery._DiscoverDeviceProfile()
self.assertListEqual(self.battery._cache['profile']['name'], ["Nexus 4"])
def testDiscoverDeviceProfile_unknown(self):
with self.patch_call(self.call.device.product_model,
return_value='Other'):
self.battery._DiscoverDeviceProfile()
self.assertListEqual(self.battery._cache['profile']['name'], [])
class BatteryUtilsClearPowerData(BatteryUtilsTest):
def testClearPowerData_preL(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=20):
self.assertFalse(self.battery._ClearPowerData())
def testClearPowerData_clearedL(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=22):
with self.assertCalls(
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True),
[]),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--reset'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--charged', '-c'],
check_return=True, large_output=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), [])):
self.assertTrue(self.battery._ClearPowerData())
@mock.patch('time.sleep', mock.Mock())
def testClearPowerData_notClearedL(self):
with self.patch_call(self.call.device.build_version_sdk,
return_value=22):
with self.assertCalls(
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'usb', '1'], check_return=True),
[]),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'set', 'ac', '1'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--reset'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--charged', '-c'],
check_return=True, large_output=True),
['9,1000,l,pwi,uid,0.0327']),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--reset'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--charged', '-c'],
check_return=True, large_output=True),
['9,1000,l,pwi,uid,0.0327']),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--reset'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--charged', '-c'],
check_return=True, large_output=True),
['9,1000,l,pwi,uid,0.0327']),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--reset'], check_return=True), []),
(self.call.device.RunShellCommand(
['dumpsys', 'batterystats', '--charged', '-c'],
check_return=True, large_output=True),
['9,1000,l,pwi,uid,0.0']),
(self.call.device.RunShellCommand(
['dumpsys', 'battery', 'reset'], check_return=True), [])):
self.battery._ClearPowerData()
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)

View File

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

View File

@@ -0,0 +1,52 @@
# Copyright 2016 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
PackageInfo = collections.namedtuple(
'PackageInfo',
['package', 'activity', 'cmdline_file', 'devtools_socket'])
PACKAGE_INFO = {
'chrome_document': PackageInfo(
'com.google.android.apps.chrome.document',
'com.google.android.apps.chrome.document.ChromeLauncherActivity',
'chrome-command-line',
'chrome_devtools_remote'),
'chrome': PackageInfo(
'com.google.android.apps.chrome',
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
'chrome_beta': PackageInfo(
'com.chrome.beta',
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
'chrome_stable': PackageInfo(
'com.android.chrome',
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
'chrome_dev': PackageInfo(
'com.chrome.dev',
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
'chrome_canary': PackageInfo(
'com.chrome.canary',
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
'chromium': PackageInfo(
'org.chromium.chrome',
'com.google.android.apps.chrome.Main',
'chrome-command-line',
'chrome_devtools_remote'),
'content_shell': PackageInfo(
'org.chromium.content_shell_apk',
'.ContentShellActivity',
'content-shell-command-line',
'content_shell_devtools_remote'),
}

View File

@@ -0,0 +1,5 @@
# 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.
TEST_EXECUTABLE_DIR = '/data/local/tmp'

View File

@@ -0,0 +1,6 @@
# Copyright 2017 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.
WEBAPK_MAIN_ACTIVITY = 'org.chromium.webapk.shell_apk.MainActivity'

View File

@@ -0,0 +1,154 @@
# Copyright 2019 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 device interactions for CPU temperature monitoring."""
# pylint: disable=unused-argument
import logging
from devil.android import device_utils
from devil.android.perf import perf_control
from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
# NB: when adding devices to this structure, be aware of the impact it may
# have on the chromium.perf waterfall, as it may increase testing time.
# Please contact a person responsible for the waterfall to see if the
# device you're adding is currently being tested.
_DEVICE_THERMAL_INFORMATION = {
# Pixel 3
'blueline': {
'cpu_temps': {
# See /sys/class/thermal/thermal_zone<number>/type for description
# Types:
# cpu0: cpu0-silver-step
# cpu1: cpu1-silver-step
# cpu2: cpu2-silver-step
# cpu3: cpu3-silver-step
# cpu4: cpu0-gold-step
# cpu5: cpu1-gold-step
# cpu6: cpu2-gold-step
# cpu7: cpu3-gold-step
'cpu0': '/sys/class/thermal/thermal_zone11/temp',
'cpu1': '/sys/class/thermal/thermal_zone12/temp',
'cpu2': '/sys/class/thermal/thermal_zone13/temp',
'cpu3': '/sys/class/thermal/thermal_zone14/temp',
'cpu4': '/sys/class/thermal/thermal_zone15/temp',
'cpu5': '/sys/class/thermal/thermal_zone16/temp',
'cpu6': '/sys/class/thermal/thermal_zone17/temp',
'cpu7': '/sys/class/thermal/thermal_zone18/temp'
},
# Different device sensors use different multipliers
# e.g. Pixel 3 35 degrees c is 35000
'temp_multiplier': 1000
},
# Pixel
'sailfish': {
'cpu_temps': {
# The following thermal zones tend to produce the most accurate
# readings
# Types:
# cpu0: tsens_tz_sensor0
# cpu1: tsens_tz_sensor1
# cpu2: tsens_tz_sensor2
# cpu3: tsens_tz_sensor3
'cpu0': '/sys/class/thermal/thermal_zone1/temp',
'cpu1': '/sys/class/thermal/thermal_zone2/temp',
'cpu2': '/sys/class/thermal/thermal_zone3/temp',
'cpu3': '/sys/class/thermal/thermal_zone4/temp'
},
'temp_multiplier': 10
}
}
class CpuTemperature(object):
def __init__(self, device):
"""CpuTemperature constructor.
Args:
device: A DeviceUtils instance.
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._perf_control = perf_control.PerfControl(self._device)
self._device_info = None
def InitThermalDeviceInformation(self):
"""Init the current devices thermal information.
"""
self._device_info = _DEVICE_THERMAL_INFORMATION.get(
self._device.build_product)
def IsSupported(self):
"""Check if the current device is supported.
Returns:
True if the device is in _DEVICE_THERMAL_INFORMATION and the temp
files exist. False otherwise.
"""
# Init device info if it hasnt been manually initialised already
if self._device_info is None:
self.InitThermalDeviceInformation()
if self._device_info is not None:
return all(
self._device.FileExists(f)
for f in self._device_info['cpu_temps'].values())
return False
def LetCpuCoolToTemperature(self, target_temp, wait_period=30):
"""Lets device sit to give CPU time to cool down.
Implements a similar mechanism to
battery_utils.LetBatteryCoolToTemperature
Args:
temp: A float containing the maximum temperature to allow
in degrees c.
wait_period: An integer indicating time in seconds to wait
between checking.
"""
target_temp = int(target_temp * self._device_info['temp_multiplier'])
def cool_cpu():
# Get the temperatures
cpu_temp_paths = self._device_info['cpu_temps']
temps = []
for temp_path in cpu_temp_paths.values():
temp_return = self._device.ReadFile(temp_path)
# Output is an array of strings, only need the first line.
temps.append(int(temp_return))
if not temps:
logger.warning('Unable to read temperature files provided.')
return True
logger.info('Current CPU temperatures: %s', str(temps)[1:-1])
return all(t <= target_temp for t in temps)
logger.info('Waiting for the CPU to cool down to %s',
target_temp / self._device_info['temp_multiplier'])
# Set the governor to powersave to aid the cooling down of the CPU
self._perf_control.SetScalingGovernor('powersave')
# Retry 3 times, each time waiting 30 seconds.
# This negates most (if not all) of the noise in recorded results without
# taking too long
timeout_retry.WaitFor(cool_cpu, wait_period=wait_period, max_tries=3)
# Set the performance mode
self._perf_control.SetHighPerfMode()
def GetDeviceForTesting(self):
return self._device
def GetDeviceInfoForTesting(self):
return self._device_info

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python
# Copyright 2019 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.
"""
Unit tests for the contents of cpu_temperature.py
"""
# pylint: disable=unused-argument
import logging
import unittest
from devil import devil_env
from devil.android import cpu_temperature
from devil.android import device_utils
from devil.utils import mock_calls
from devil.android.sdk import adb_wrapper
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
class CpuTemperatureTest(mock_calls.TestCase):
@mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
def setUp(self):
# Mock the device
self.mock_device = mock.Mock(spec=device_utils.DeviceUtils)
self.mock_device.build_product = 'blueline'
self.mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
self.mock_device.FileExists.return_value = True
self.cpu_temp = cpu_temperature.CpuTemperature(self.mock_device)
self.cpu_temp.InitThermalDeviceInformation()
class CpuTemperatureInitTest(unittest.TestCase):
@mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
def testInitWithDeviceUtil(self):
d = mock.Mock(spec=device_utils.DeviceUtils)
d.build_product = 'blueline'
c = cpu_temperature.CpuTemperature(d)
self.assertEqual(d, c.GetDeviceForTesting())
def testInitWithMissing_fails(self):
with self.assertRaises(TypeError):
cpu_temperature.CpuTemperature(None)
with self.assertRaises(TypeError):
cpu_temperature.CpuTemperature('')
class CpuTemperatureGetThermalDeviceInformationTest(CpuTemperatureTest):
@mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
def testGetThermalDeviceInformation_noneWhenIncorrectLabel(self):
invalid_device = mock.Mock(spec=device_utils.DeviceUtils)
invalid_device.build_product = 'invalid_name'
c = cpu_temperature.CpuTemperature(invalid_device)
c.InitThermalDeviceInformation()
self.assertEqual(c.GetDeviceInfoForTesting(), None)
def testGetThermalDeviceInformation_getsCorrectInformation(self):
correct_information = {
'cpu0': '/sys/class/thermal/thermal_zone11/temp',
'cpu1': '/sys/class/thermal/thermal_zone12/temp',
'cpu2': '/sys/class/thermal/thermal_zone13/temp',
'cpu3': '/sys/class/thermal/thermal_zone14/temp',
'cpu4': '/sys/class/thermal/thermal_zone15/temp',
'cpu5': '/sys/class/thermal/thermal_zone16/temp',
'cpu6': '/sys/class/thermal/thermal_zone17/temp',
'cpu7': '/sys/class/thermal/thermal_zone18/temp'
}
self.assertEqual(
cmp(correct_information,
self.cpu_temp.GetDeviceInfoForTesting().get('cpu_temps')), 0)
class CpuTemperatureIsSupportedTest(CpuTemperatureTest):
@mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
def testIsSupported_returnsTrue(self):
d = mock.Mock(spec=device_utils.DeviceUtils)
d.build_product = 'blueline'
d.FileExists.return_value = True
c = cpu_temperature.CpuTemperature(d)
self.assertTrue(c.IsSupported())
@mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
def testIsSupported_returnsFalse(self):
d = mock.Mock(spec=device_utils.DeviceUtils)
d.build_product = 'blueline'
d.FileExists.return_value = False
c = cpu_temperature.CpuTemperature(d)
self.assertFalse(c.IsSupported())
class CpuTemperatureLetCpuCoolToTemperatureTest(CpuTemperatureTest):
# Return values for the mock side effect
cooling_down0 = ([45000 for _ in range(8)] + [43000 for _ in range(8)] +
[41000 for _ in range(8)])
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_coolWithin24Calls(self):
self.mock_device.ReadFile = mock.Mock(side_effect=self.cooling_down0)
self.cpu_temp.LetCpuCoolToTemperature(42)
self.mock_device.ReadFile.assert_called()
self.assertEquals(self.mock_device.ReadFile.call_count, 24)
cooling_down1 = [45000 for _ in range(8)] + [41000 for _ in range(16)]
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_coolWithin16Calls(self):
self.mock_device.ReadFile = mock.Mock(side_effect=self.cooling_down1)
self.cpu_temp.LetCpuCoolToTemperature(42)
self.mock_device.ReadFile.assert_called()
self.assertEquals(self.mock_device.ReadFile.call_count, 16)
constant_temp = [45000 for _ in range(40)]
@mock.patch('time.sleep', mock.Mock())
def testLetBatteryCoolToTemperature_timeoutAfterThree(self):
self.mock_device.ReadFile = mock.Mock(side_effect=self.constant_temp)
self.cpu_temp.LetCpuCoolToTemperature(42)
self.mock_device.ReadFile.assert_called()
self.assertEquals(self.mock_device.ReadFile.call_count, 24)
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)

View File

@@ -0,0 +1,46 @@
# Copyright 2017 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
from devil import base_error
from devil.android import device_errors
logger = logging.getLogger(__name__)
def RetryOnSystemCrash(f, device, retries=3):
"""Retries the given function on a device crash.
If the provided function fails with a DeviceUnreachableError, this will wait
for the device to come back online, then retry the function.
Note that this uses the same retry scheme as timeout_retry.Run.
Args:
f: a unary callable that takes an instance of device_utils.DeviceUtils.
device: an instance of device_utils.DeviceUtils.
retries: the number of retries.
Returns:
Whatever f returns.
"""
num_try = 1
while True:
try:
return f(device)
except device_errors.DeviceUnreachableError:
if num_try > retries:
logger.error('%d consecutive device crashes. No longer retrying.',
num_try)
raise
try:
logger.warning('Device is unreachable. Waiting for recovery...')
# Treat the device being unreachable as an unexpected reboot and clear
# any cached state.
device.ClearCache()
device.WaitUntilFullyBooted()
except base_error.BaseError:
logger.exception('Device never recovered. X(')
num_try += 1

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', )))
from devil.android import crash_handler
from devil.android import device_errors
from devil.android import device_utils
from devil.android import device_temp_file
from devil.android import device_test_case
from devil.utils import cmd_helper
from devil.utils import reraiser_thread
from devil.utils import timeout_retry
class DeviceCrashTest(device_test_case.DeviceTestCase):
def setUp(self):
super(DeviceCrashTest, self).setUp()
self.device = device_utils.DeviceUtils(self.serial)
def testCrashDuringCommand(self):
self.device.EnableRoot()
with device_temp_file.DeviceTempFile(self.device.adb) as trigger_file:
trigger_text = 'hello world'
def victim():
trigger_cmd = 'echo -n %s > %s; sleep 20' % (
cmd_helper.SingleQuote(trigger_text),
cmd_helper.SingleQuote(trigger_file.name))
crash_handler.RetryOnSystemCrash(
lambda d: d.RunShellCommand(
trigger_cmd, shell=True, check_return=True, retries=1,
as_root=True, timeout=180),
device=self.device)
self.assertEquals(
trigger_text,
self.device.ReadFile(trigger_file.name, retries=0).strip())
return True
def crasher():
def ready_to_crash():
try:
return trigger_text == self.device.ReadFile(
trigger_file.name, retries=0).strip()
except device_errors.CommandFailedError:
return False
timeout_retry.WaitFor(ready_to_crash, wait_period=2, max_tries=10)
if not ready_to_crash():
return False
self.device.adb.Shell(
'echo c > /proc/sysrq-trigger',
expect_status=None, timeout=60, retries=0)
return True
self.assertEquals([True, True],
reraiser_thread.RunAsync([crasher, victim]))
if __name__ == '__main__':
device_test_case.PrepareDevices()
unittest.main()

View File

@@ -0,0 +1,176 @@
# 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 itertools
import sys
from devil.android import device_errors
from devil.utils import cmd_helper
from devil.utils import reraiser_thread
from devil.utils import timeout_retry
DEFAULT_TIMEOUT_ATTR = '_default_timeout'
DEFAULT_RETRIES_ATTR = '_default_retries'
def _TimeoutRetryWrapper(
f, timeout_func, retries_func, retry_if_func=timeout_retry.AlwaysRetry,
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 timeout_retry_wrapper(*args, **kwargs):
timeout = timeout_func(*args, **kwargs)
retries = retries_func(*args, **kwargs)
if pass_values:
kwargs['timeout'] = timeout
kwargs['retries'] = retries
@functools.wraps(f)
def impl():
return f(*args, **kwargs)
try:
if timeout_retry.CurrentTimeoutThreadGroup():
# Don't wrap if there's already an outer timeout thread.
return impl()
else:
desc = '%s(%s)' % (f.__name__, ', '.join(itertools.chain(
(str(a) for a in args),
('%s=%s' % (k, str(v)) for k, v in kwargs.iteritems()))))
return timeout_retry.Run(impl, timeout, retries, desc=desc,
retry_if_func=retry_if_func)
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), output=e.output), None, (
sys.exc_info()[2])
return timeout_retry_wrapper
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 WithTimeoutAndConditionalRetries(retry_if_func):
"""Returns a decorator that handles timeouts and, in some cases, retries.
'timeout' and 'retries' kwargs must be passed to the function.
Args:
retry_if_func: A unary callable that takes an exception and returns
whether failures should be retried.
Returns:
The actual decorator.
"""
def decorator(f):
get_timeout = lambda *a, **kw: kw['timeout']
get_retries = lambda *a, **kw: kw['retries']
return _TimeoutRetryWrapper(
f, get_timeout, get_retries, retry_if_func=retry_if_func)
return decorator
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,
min_default_timeout=None):
"""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.
min_timeout: Miniumum timeout to be used when using instance timeout.
Returns:
The actual decorator.
"""
def decorator(f):
def get_timeout(inst, *_args, **kwargs):
ret = getattr(inst, default_timeout_name)
if min_default_timeout is not None:
ret = max(min_default_timeout, ret)
return kwargs.get('timeout', ret)
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

View File

@@ -0,0 +1,332 @@
# 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.
"""
Unit tests for decorators.py.
"""
# pylint: disable=W0613
import time
import traceback
import unittest
from devil.android import decorators
from devil.android import device_errors
from devil.utils import reraiser_thread
_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3
class DecoratorsTest(unittest.TestCase):
_decorated_function_called_count = 0
def testFunctionDecoratorDoesTimeouts(self):
"""Tests that the base decorator handles the timeout logic."""
DecoratorsTest._decorated_function_called_count = 0
@decorators.WithTimeoutAndRetries
def alwaysTimesOut(timeout=None, retries=None):
DecoratorsTest._decorated_function_called_count += 1
time.sleep(100)
start_time = time.time()
with self.assertRaises(device_errors.CommandTimeoutError):
alwaysTimesOut(timeout=1, retries=0)
elapsed_time = time.time() - start_time
self.assertTrue(elapsed_time >= 1)
self.assertEquals(1, DecoratorsTest._decorated_function_called_count)
def testFunctionDecoratorDoesRetries(self):
"""Tests that the base decorator handles the retries logic."""
DecoratorsTest._decorated_function_called_count = 0
@decorators.WithTimeoutAndRetries
def alwaysRaisesCommandFailedError(timeout=None, retries=None):
DecoratorsTest._decorated_function_called_count += 1
raise device_errors.CommandFailedError('testCommand failed')
with self.assertRaises(device_errors.CommandFailedError):
alwaysRaisesCommandFailedError(timeout=30, retries=10)
self.assertEquals(11, DecoratorsTest._decorated_function_called_count)
def testFunctionDecoratorRequiresParams(self):
"""Tests that the base decorator requires timeout and retries params."""
@decorators.WithTimeoutAndRetries
def requiresExplicitTimeoutAndRetries(timeout=None, retries=None):
return (timeout, retries)
with self.assertRaises(KeyError):
requiresExplicitTimeoutAndRetries()
with self.assertRaises(KeyError):
requiresExplicitTimeoutAndRetries(timeout=10)
with self.assertRaises(KeyError):
requiresExplicitTimeoutAndRetries(retries=0)
expected_timeout = 10
expected_retries = 1
(actual_timeout, actual_retries) = (
requiresExplicitTimeoutAndRetries(timeout=expected_timeout,
retries=expected_retries))
self.assertEquals(expected_timeout, actual_timeout)
self.assertEquals(expected_retries, actual_retries)
def testFunctionDecoratorTranslatesReraiserExceptions(self):
"""Tests that the explicit decorator translates reraiser exceptions."""
@decorators.WithTimeoutAndRetries
def alwaysRaisesProvidedException(exception, timeout=None, retries=None):
raise exception
exception_desc = 'Reraiser thread timeout error'
with self.assertRaises(device_errors.CommandTimeoutError) as e:
alwaysRaisesProvidedException(
reraiser_thread.TimeoutError(exception_desc),
timeout=10, retries=1)
self.assertEquals(exception_desc, str(e.exception))
def testConditionalRetriesDecoratorRetries(self):
def do_not_retry_no_adb_error(exc):
return not isinstance(exc, device_errors.NoAdbError)
actual_tries = [0]
@decorators.WithTimeoutAndConditionalRetries(do_not_retry_no_adb_error)
def alwaysRaisesCommandFailedError(timeout=None, retries=None):
actual_tries[0] += 1
raise device_errors.CommandFailedError('Command failed :(')
with self.assertRaises(device_errors.CommandFailedError):
alwaysRaisesCommandFailedError(timeout=10, retries=10)
self.assertEquals(11, actual_tries[0])
def testConditionalRetriesDecoratorDoesntRetry(self):
def do_not_retry_no_adb_error(exc):
return not isinstance(exc, device_errors.NoAdbError)
actual_tries = [0]
@decorators.WithTimeoutAndConditionalRetries(do_not_retry_no_adb_error)
def alwaysRaisesNoAdbError(timeout=None, retries=None):
actual_tries[0] += 1
raise device_errors.NoAdbError()
with self.assertRaises(device_errors.NoAdbError):
alwaysRaisesNoAdbError(timeout=10, retries=10)
self.assertEquals(1, actual_tries[0])
def testDefaultsFunctionDecoratorDoesTimeouts(self):
"""Tests that the defaults decorator handles timeout logic."""
DecoratorsTest._decorated_function_called_count = 0
@decorators.WithTimeoutAndRetriesDefaults(1, 0)
def alwaysTimesOut(timeout=None, retries=None):
DecoratorsTest._decorated_function_called_count += 1
time.sleep(100)
start_time = time.time()
with self.assertRaises(device_errors.CommandTimeoutError):
alwaysTimesOut()
elapsed_time = time.time() - start_time
self.assertTrue(elapsed_time >= 1)
self.assertEquals(1, DecoratorsTest._decorated_function_called_count)
DecoratorsTest._decorated_function_called_count = 0
with self.assertRaises(device_errors.CommandTimeoutError):
alwaysTimesOut(timeout=2)
elapsed_time = time.time() - start_time
self.assertTrue(elapsed_time >= 2)
self.assertEquals(1, DecoratorsTest._decorated_function_called_count)
def testDefaultsFunctionDecoratorDoesRetries(self):
"""Tests that the defaults decorator handles retries logic."""
DecoratorsTest._decorated_function_called_count = 0
@decorators.WithTimeoutAndRetriesDefaults(30, 10)
def alwaysRaisesCommandFailedError(timeout=None, retries=None):
DecoratorsTest._decorated_function_called_count += 1
raise device_errors.CommandFailedError('testCommand failed')
with self.assertRaises(device_errors.CommandFailedError):
alwaysRaisesCommandFailedError()
self.assertEquals(11, DecoratorsTest._decorated_function_called_count)
DecoratorsTest._decorated_function_called_count = 0
with self.assertRaises(device_errors.CommandFailedError):
alwaysRaisesCommandFailedError(retries=5)
self.assertEquals(6, DecoratorsTest._decorated_function_called_count)
def testDefaultsFunctionDecoratorPassesValues(self):
"""Tests that the defaults decorator passes timeout and retries kwargs."""
@decorators.WithTimeoutAndRetriesDefaults(30, 10)
def alwaysReturnsTimeouts(timeout=None, retries=None):
return timeout
self.assertEquals(30, alwaysReturnsTimeouts())
self.assertEquals(120, alwaysReturnsTimeouts(timeout=120))
@decorators.WithTimeoutAndRetriesDefaults(30, 10)
def alwaysReturnsRetries(timeout=None, retries=None):
return retries
self.assertEquals(10, alwaysReturnsRetries())
self.assertEquals(1, alwaysReturnsRetries(retries=1))
def testDefaultsFunctionDecoratorTranslatesReraiserExceptions(self):
"""Tests that the explicit decorator translates reraiser exceptions."""
@decorators.WithTimeoutAndRetriesDefaults(30, 10)
def alwaysRaisesProvidedException(exception, timeout=None, retries=None):
raise exception
exception_desc = 'Reraiser thread timeout error'
with self.assertRaises(device_errors.CommandTimeoutError) as e:
alwaysRaisesProvidedException(
reraiser_thread.TimeoutError(exception_desc))
self.assertEquals(exception_desc, str(e.exception))
def testExplicitFunctionDecoratorDoesTimeouts(self):
"""Tests that the explicit decorator handles timeout logic."""
DecoratorsTest._decorated_function_called_count = 0
@decorators.WithExplicitTimeoutAndRetries(1, 0)
def alwaysTimesOut():
DecoratorsTest._decorated_function_called_count += 1
time.sleep(100)
start_time = time.time()
with self.assertRaises(device_errors.CommandTimeoutError):
alwaysTimesOut()
elapsed_time = time.time() - start_time
self.assertTrue(elapsed_time >= 1)
self.assertEquals(1, DecoratorsTest._decorated_function_called_count)
def testExplicitFunctionDecoratorDoesRetries(self):
"""Tests that the explicit decorator handles retries logic."""
DecoratorsTest._decorated_function_called_count = 0
@decorators.WithExplicitTimeoutAndRetries(30, 10)
def alwaysRaisesCommandFailedError():
DecoratorsTest._decorated_function_called_count += 1
raise device_errors.CommandFailedError('testCommand failed')
with self.assertRaises(device_errors.CommandFailedError):
alwaysRaisesCommandFailedError()
self.assertEquals(11, DecoratorsTest._decorated_function_called_count)
def testExplicitDecoratorTranslatesReraiserExceptions(self):
"""Tests that the explicit decorator translates reraiser exceptions."""
@decorators.WithExplicitTimeoutAndRetries(30, 10)
def alwaysRaisesProvidedException(exception):
raise exception
exception_desc = 'Reraiser thread timeout error'
with self.assertRaises(device_errors.CommandTimeoutError) as e:
alwaysRaisesProvidedException(
reraiser_thread.TimeoutError(exception_desc))
self.assertEquals(exception_desc, str(e.exception))
class _MethodDecoratorTestObject(object):
"""An object suitable for testing the method decorator."""
def __init__(self, test_case, default_timeout=_DEFAULT_TIMEOUT,
default_retries=_DEFAULT_RETRIES):
self._test_case = test_case
self.default_timeout = default_timeout
self.default_retries = default_retries
self.function_call_counters = {
'alwaysRaisesCommandFailedError': 0,
'alwaysTimesOut': 0,
'requiresExplicitTimeoutAndRetries': 0,
}
@decorators.WithTimeoutAndRetriesFromInstance(
'default_timeout', 'default_retries')
def alwaysTimesOut(self, timeout=None, retries=None):
self.function_call_counters['alwaysTimesOut'] += 1
time.sleep(100)
self._test_case.assertFalse(True, msg='Failed to time out?')
@decorators.WithTimeoutAndRetriesFromInstance(
'default_timeout', 'default_retries')
def alwaysRaisesCommandFailedError(self, timeout=None, retries=None):
self.function_call_counters['alwaysRaisesCommandFailedError'] += 1
raise device_errors.CommandFailedError('testCommand failed')
# pylint: disable=no-self-use
@decorators.WithTimeoutAndRetriesFromInstance(
'default_timeout', 'default_retries')
def alwaysReturnsTimeout(self, timeout=None, retries=None):
return timeout
@decorators.WithTimeoutAndRetriesFromInstance(
'default_timeout', 'default_retries', min_default_timeout=100)
def alwaysReturnsTimeoutWithMin(self, timeout=None, retries=None):
return timeout
@decorators.WithTimeoutAndRetriesFromInstance(
'default_timeout', 'default_retries')
def alwaysReturnsRetries(self, timeout=None, retries=None):
return retries
@decorators.WithTimeoutAndRetriesFromInstance(
'default_timeout', 'default_retries')
def alwaysRaisesProvidedException(self, exception, timeout=None,
retries=None):
raise exception
# pylint: enable=no-self-use
def testMethodDecoratorDoesTimeout(self):
"""Tests that the method decorator handles timeout logic."""
test_obj = self._MethodDecoratorTestObject(self)
start_time = time.time()
with self.assertRaises(device_errors.CommandTimeoutError):
try:
test_obj.alwaysTimesOut(timeout=1, retries=0)
except:
traceback.print_exc()
raise
elapsed_time = time.time() - start_time
self.assertTrue(elapsed_time >= 1)
self.assertEquals(1, test_obj.function_call_counters['alwaysTimesOut'])
def testMethodDecoratorDoesRetries(self):
"""Tests that the method decorator handles retries logic."""
test_obj = self._MethodDecoratorTestObject(self)
with self.assertRaises(device_errors.CommandFailedError):
try:
test_obj.alwaysRaisesCommandFailedError(retries=10)
except:
traceback.print_exc()
raise
self.assertEquals(
11, test_obj.function_call_counters['alwaysRaisesCommandFailedError'])
def testMethodDecoratorPassesValues(self):
"""Tests that the method decorator passes timeout and retries kwargs."""
test_obj = self._MethodDecoratorTestObject(
self, default_timeout=42, default_retries=31)
self.assertEquals(42, test_obj.alwaysReturnsTimeout())
self.assertEquals(41, test_obj.alwaysReturnsTimeout(timeout=41))
self.assertEquals(31, test_obj.alwaysReturnsRetries())
self.assertEquals(32, test_obj.alwaysReturnsRetries(retries=32))
def testMethodDecoratorUsesMiniumumTimeout(self):
test_obj = self._MethodDecoratorTestObject(
self, default_timeout=42, default_retries=31)
self.assertEquals(100, test_obj.alwaysReturnsTimeoutWithMin())
self.assertEquals(41, test_obj.alwaysReturnsTimeoutWithMin(timeout=41))
def testMethodDecoratorTranslatesReraiserExceptions(self):
test_obj = self._MethodDecoratorTestObject(self)
exception_desc = 'Reraiser thread timeout error'
with self.assertRaises(device_errors.CommandTimeoutError) as e:
test_obj.alwaysRaisesProvidedException(
reraiser_thread.TimeoutError(exception_desc))
self.assertEquals(exception_desc, str(e.exception))
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,80 @@
# 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 logging
import os
import threading
import time
logger = logging.getLogger(__name__)
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 dict containing bad devices.
"""
with self._blacklist_lock:
blacklist = dict()
if not os.path.exists(self._path):
return blacklist
try:
with open(self._path, 'r') as f:
blacklist = json.load(f)
except (IOError, ValueError) as e:
logger.warning('Unable to read blacklist: %s', str(e))
os.remove(self._path)
if not isinstance(blacklist, dict):
logger.warning('Ignoring %s: %s (a dict was expected instead)',
self._path, blacklist)
blacklist = dict()
return blacklist
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(blacklist, f)
def Extend(self, devices, reason='unknown'):
"""Adds devices to blacklist file.
Args:
devices: list of bad devices to be added to the blacklist file.
reason: string specifying the reason for blacklist (eg: 'unauthorized')
"""
timestamp = time.time()
event_info = {
'timestamp': timestamp,
'reason': reason,
}
device_dicts = {device: event_info for device in devices}
logger.info('Adding %s to blacklist %s for reason: %s',
','.join(devices), self._path, reason)
with self._blacklist_lock:
blacklist = self.Read()
blacklist.update(device_dicts)
self.Write(blacklist)
def Reset(self):
"""Erases the blacklist file if it exists."""
logger.info('Resetting blacklist %s', self._path)
with self._blacklist_lock:
if os.path.exists(self._path):
os.remove(self._path)

View File

@@ -0,0 +1,38 @@
#! /usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import tempfile
import unittest
from devil.android import device_blacklist
class DeviceBlacklistTest(unittest.TestCase):
def testBlacklistFileDoesNotExist(self):
with tempfile.NamedTemporaryFile() as blacklist_file:
# Allow the temporary file to be deleted.
pass
test_blacklist = device_blacklist.Blacklist(blacklist_file.name)
self.assertEquals({}, test_blacklist.Read())
def testBlacklistFileIsEmpty(self):
try:
with tempfile.NamedTemporaryFile(delete=False) as blacklist_file:
# Allow the temporary file to be closed.
pass
test_blacklist = device_blacklist.Blacklist(blacklist_file.name)
self.assertEquals({}, test_blacklist.Read())
finally:
if os.path.exists(blacklist_file.name):
os.remove(blacklist_file.name)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,196 @@
# 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.
The class hierarchy for device exceptions is:
base_error.BaseError
+-- CommandFailedError
| +-- AdbCommandFailedError
| | +-- AdbShellCommandFailedError
| +-- FastbootCommandFailedError
| +-- DeviceVersionError
| +-- DeviceChargingError
+-- CommandTimeoutError
+-- DeviceUnreachableError
+-- NoDevicesError
+-- MultipleDevicesError
+-- NoAdbError
"""
from devil import base_error
from devil.utils import cmd_helper
from devil.utils import parallelizer
class CommandFailedError(base_error.BaseError):
"""Exception for command failures."""
def __init__(self, message, device_serial=None):
device_leader = '(device: %s)' % device_serial
if device_serial is not None and not message.startswith(device_leader):
message = '%s %s' % (device_leader, message)
self.device_serial = device_serial
super(CommandFailedError, self).__init__(message)
def __eq__(self, other):
return (super(CommandFailedError, self).__eq__(other)
and self.device_serial == other.device_serial)
def __ne__(self, other):
return not self == other
class _BaseCommandFailedError(CommandFailedError):
"""Base Exception for adb and fastboot 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)
segments = ['adb %s: failed ' % adb_cmd]
if status:
segments.append('with exit status %s ' % self.status)
if output:
segments.append('and output:\n')
segments.extend('- %s\n' % line for line in output.splitlines())
else:
segments.append('and no output.')
message = ''.join(segments)
super(_BaseCommandFailedError, self).__init__(message, device_serial)
def __eq__(self, other):
return (super(_BaseCommandFailedError, self).__eq__(other)
and self.args == other.args
and self.output == other.output
and self.status == other.status)
def __ne__(self, other):
return not self == other
def __reduce__(self):
"""Support pickling."""
result = [None, None, None, None, None]
super_result = super(_BaseCommandFailedError, self).__reduce__()
result[:len(super_result)] = super_result
# Update the args used to reconstruct this exception.
result[1] = (
self.args, self.output, self.status, self.device_serial, self.message)
return tuple(result)
class AdbCommandFailedError(_BaseCommandFailedError):
"""Exception for adb command failures."""
def __init__(self, args, output, status=None, device_serial=None,
message=None):
super(AdbCommandFailedError, self).__init__(
args, output, status=status, message=message,
device_serial=device_serial)
class FastbootCommandFailedError(_BaseCommandFailedError):
"""Exception for fastboot command failures."""
def __init__(self, args, output, status=None, device_serial=None,
message=None):
super(FastbootCommandFailedError, self).__init__(
args, output, status=status, message=message,
device_serial=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
segments = ['shell command run via adb failed on the device:\n',
' command: %s\n' % command]
segments.append(' exit status: %s\n' % status)
if output:
segments.append(' output:\n')
if isinstance(output, basestring):
output_lines = output.splitlines()
else:
output_lines = output
segments.extend(' - %s\n' % line for line in output_lines)
else:
segments.append(" output: ''\n")
message = ''.join(segments)
super(AdbShellCommandFailedError, self).__init__(
['shell', command], output, status, device_serial, message)
def __reduce__(self):
"""Support pickling."""
result = [None, None, None, None, None]
super_result = super(AdbShellCommandFailedError, self).__reduce__()
result[:len(super_result)] = super_result
# Update the args used to reconstruct this exception.
result[1] = (self.command, self.output, self.status, self.device_serial)
return tuple(result)
class CommandTimeoutError(base_error.BaseError):
"""Exception for command timeouts."""
def __init__(self, message, is_infra_error=False, output=None):
super(CommandTimeoutError, self).__init__(message, is_infra_error)
self.output = output
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, msg=None):
super(NoDevicesError, self).__init__(
msg or 'No devices attached.', is_infra_error=True)
class MultipleDevicesError(base_error.BaseError):
"""Exception for having multiple attached devices without selecting one."""
def __init__(self, devices):
parallel_devices = parallelizer.Parallelizer(devices)
descriptions = parallel_devices.pMap(
lambda d: d.build_description).pGet(None)
msg = ('More than one device available. Use -d/--device to select a device '
'by serial.\n\nAvailable devices:\n')
for d, desc in zip(devices, descriptions):
msg += ' %s (%s)\n' % (d, desc)
super(MultipleDevicesError, self).__init__(msg, is_infra_error=True)
class NoAdbError(base_error.BaseError):
"""Exception for being unable to find ADB."""
def __init__(self, msg=None):
super(NoAdbError, self).__init__(
msg or 'Unable to find adb.', is_infra_error=True)
class DeviceChargingError(CommandFailedError):
"""Exception for device charging errors."""
def __init__(self, message, device_serial=None):
super(DeviceChargingError, self).__init__(message, device_serial)

View File

@@ -0,0 +1,72 @@
#! /usr/bin/env python
# Copyright 2017 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 pickle
import sys
import unittest
from devil.android import device_errors
class DeviceErrorsTest(unittest.TestCase):
def assertIsPicklable(self, original):
pickled = pickle.dumps(original)
reconstructed = pickle.loads(pickled)
self.assertEquals(original, reconstructed)
def testPicklable_AdbCommandFailedError(self):
original = device_errors.AdbCommandFailedError(
['these', 'are', 'adb', 'args'], 'adb failure output', status=':(',
device_serial='0123456789abcdef')
self.assertIsPicklable(original)
def testPicklable_AdbShellCommandFailedError(self):
original = device_errors.AdbShellCommandFailedError(
'foo', 'erroneous foo output', '1', device_serial='0123456789abcdef')
self.assertIsPicklable(original)
def testPicklable_CommandFailedError(self):
original = device_errors.CommandFailedError(
'sample command failed')
self.assertIsPicklable(original)
def testPicklable_CommandTimeoutError(self):
original = device_errors.CommandTimeoutError(
'My fake command timed out :(')
self.assertIsPicklable(original)
def testPicklable_DeviceChargingError(self):
original = device_errors.DeviceChargingError(
'Fake device failed to charge')
self.assertIsPicklable(original)
def testPicklable_DeviceUnreachableError(self):
original = device_errors.DeviceUnreachableError
self.assertIsPicklable(original)
def testPicklable_FastbootCommandFailedError(self):
original = device_errors.FastbootCommandFailedError(
['these', 'are', 'fastboot', 'args'], 'fastboot failure output',
status=':(', device_serial='0123456789abcdef')
self.assertIsPicklable(original)
def testPicklable_MultipleDevicesError(self):
# TODO(jbudorick): Implement this after implementing a stable DeviceUtils
# fake. https://github.com/catapult-project/catapult/issues/3145
pass
def testPicklable_NoAdbError(self):
original = device_errors.NoAdbError()
self.assertIsPicklable(original)
def testPicklable_NoDevicesError(self):
original = device_errors.NoDevicesError()
self.assertIsPicklable(original)
if __name__ == '__main__':
sys.exit(unittest.main())

View File

@@ -0,0 +1,52 @@
# 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 json
import logging
import os
logger = logging.getLogger(__name__)
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.
"""
if not os.path.isfile(file_name):
logger.warning("Device file %s doesn't exist.", file_name)
return []
try:
with open(file_name) as f:
devices = json.load(f)
if not isinstance(devices, list) or not all(isinstance(d, basestring)
for d in devices):
logger.warning('Unrecognized device file format: %s', devices)
return []
return [d for d in devices if d != '(error)']
except ValueError:
logger.exception(
'Error reading device file %s. Falling back to old format.', file_name)
# TODO(bpastene) Remove support for old unstructured file format.
with open(file_name) as f:
return [d for d in f.read().splitlines() if d != '(error)']
def WritePersistentDeviceList(file_name, device_list):
path = os.path.dirname(file_name)
assert isinstance(device_list, list)
# If there is a problem with ADB "(error)" can be added to the device list.
# These should be removed before saving.
device_list = [d for d in device_list if d != '(error)']
if not os.path.exists(path):
os.makedirs(path)
with open(file_name, 'w') as f:
json.dump(device_list, f)

View File

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

View File

@@ -0,0 +1,119 @@
# 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 logging
import posixpath
import random
import threading
from devil import base_error
from devil.android import device_errors
from devil.utils import cmd_helper
logger = logging.getLogger(__name__)
def _GenerateName(prefix, suffix, dir):
random_hex = hex(random.randint(0, 2 ** 52))[2:]
return posixpath.join(dir, '%s-%s%s' % (prefix, random_hex, suffix))
class DeviceTempFile(object):
"""A named temporary file on a device.
Behaves like tempfile.NamedTemporaryFile.
"""
def __init__(self, adb, suffix='', prefix='temp_file', dir='/data/local/tmp'):
"""Find an unused temporary file path on the device.
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 temporary file.
prefix: The prefix of the name of the temporary file.
dir: The directory on the device in which the temporary file should be
placed.
Raises:
ValueError if any of suffix, prefix, or dir are None.
"""
if None in (dir, prefix, suffix):
m = 'Provided None path component. (dir: %s, prefix: %s, suffix: %s)' % (
dir, prefix, suffix)
raise ValueError(m)
self._adb = adb
# Python's random module use 52-bit numbers according to its docs.
self.name = _GenerateName(prefix, suffix, dir)
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 delete_temporary_file():
try:
self._adb.Shell('rm -f %s' % self.name_quoted, expect_status=None)
except base_error.BaseError as e:
# We don't really care, and stack traces clog up the log.
# Log a warning and move on.
logger.warning('Failed to delete temporary file %s: %s',
self.name, str(e))
# It shouldn't matter when the temp file gets deleted, so do so
# asynchronously.
threading.Thread(
target=delete_temporary_file,
name='delete_temporary_file(%s)' % self._adb.GetDeviceSerial()).start()
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.close()
class NamedDeviceTemporaryDirectory(object):
"""A named temporary directory on a device."""
def __init__(self, adb, suffix='', prefix='tmp', dir='/data/local/tmp'):
"""Find an unused temporary directory path on the device. The directory is
not created until it is used with a 'with' statement.
When this object is closed, the directory will be deleted on the device.
Args:
adb: An instance of AdbWrapper
suffix: The suffix of the name of the temporary directory.
prefix: The prefix of the name of the temporary directory.
dir: The directory on the device where to place the temporary directory.
Raises:
ValueError if any of suffix, prefix, or dir are None.
"""
self._adb = adb
self.name = _GenerateName(prefix, suffix, dir)
self.name_quoted = cmd_helper.SingleQuote(self.name)
def close(self):
"""Deletes the temporary directory from the device."""
def delete_temporary_dir():
try:
self._adb.Shell('rm -rf %s' % self.name, expect_status=None)
except device_errors.AdbCommandFailedError:
pass
threading.Thread(
target=delete_temporary_dir,
name='delete_temporary_dir(%s)' % self._adb.GetDeviceSerial()).start()
def __enter__(self):
self._adb.Shell('mkdir -p %s' % self.name)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()

View File

@@ -0,0 +1,54 @@
# Copyright 2016 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 threading
import unittest
from devil.android import device_errors
from devil.android import device_utils
_devices_lock = threading.Lock()
_devices_condition = threading.Condition(_devices_lock)
_devices = set()
def PrepareDevices(*_args):
raw_devices = device_utils.DeviceUtils.HealthyDevices()
live_devices = []
for d in raw_devices:
try:
d.WaitUntilFullyBooted(timeout=5, retries=0)
live_devices.append(str(d))
except (device_errors.CommandFailedError,
device_errors.CommandTimeoutError,
device_errors.DeviceUnreachableError):
pass
with _devices_lock:
_devices.update(set(live_devices))
if not _devices:
raise Exception('No live devices attached.')
class DeviceTestCase(unittest.TestCase):
def __init__(self, *args, **kwargs):
super(DeviceTestCase, self).__init__(*args, **kwargs)
self.serial = None
#override
def setUp(self):
super(DeviceTestCase, self).setUp()
with _devices_lock:
while not _devices:
_devices_condition.wait(5)
self.serial = _devices.pop()
#override
def tearDown(self):
super(DeviceTestCase, self).tearDown()
with _devices_lock:
_devices.add(self.serial)
_devices_condition.notify()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,301 @@
#!/usr/bin/env python
# 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.
"""
Unit tests for the contents of device_utils.py (mostly DeviceUtils).
The test will invoke real devices
"""
import os
import posixpath
import sys
import tempfile
import unittest
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', )))
from devil.android import device_test_case
from devil.android import device_utils
from devil.android.sdk import adb_wrapper
from devil.utils import cmd_helper
_OLD_CONTENTS = "foo"
_NEW_CONTENTS = "bar"
_DEVICE_DIR = "/data/local/tmp/device_utils_test"
_SUB_DIR = "sub"
_SUB_DIR1 = "sub1"
_SUB_DIR2 = "sub2"
class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
def setUp(self):
super(DeviceUtilsPushDeleteFilesTest, self).setUp()
self.adb = adb_wrapper.AdbWrapper(self.serial)
self.adb.WaitForDevice()
self.device = device_utils.DeviceUtils(
self.adb, default_timeout=10, default_retries=0)
@staticmethod
def _MakeTempFile(contents):
"""Make a temporary file with the given contents.
Args:
contents: string to write to the temporary file.
Returns:
the tuple contains the absolute path to the file and the file name
"""
fi, path = tempfile.mkstemp(text=True)
with os.fdopen(fi, 'w') as f:
f.write(contents)
file_name = os.path.basename(path)
return (path, file_name)
@staticmethod
def _MakeTempFileGivenDir(directory, contents):
"""Make a temporary file under the given directory
with the given contents
Args:
directory: the temp directory to create the file
contents: string to write to the temp file
Returns:
the list contains the absolute path to the file and the file name
"""
fi, path = tempfile.mkstemp(dir=directory, text=True)
with os.fdopen(fi, 'w') as f:
f.write(contents)
file_name = os.path.basename(path)
return (path, file_name)
@staticmethod
def _ChangeTempFile(path, contents):
with os.open(path, 'w') as f:
f.write(contents)
@staticmethod
def _DeleteTempFile(path):
os.remove(path)
def testPushChangedFiles_noFileChange(self):
(host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
self.adb.Push(host_file_path, device_file_path)
self.device.PushChangedFiles([(host_file_path, device_file_path)])
result = self.device.RunShellCommand(
['cat', device_file_path], check_return=True, single_line=True)
self.assertEqual(_OLD_CONTENTS, result)
cmd_helper.RunCmd(['rm', host_file_path])
self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testPushChangedFiles_singleFileChange(self):
(host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
self.adb.Push(host_file_path, device_file_path)
with open(host_file_path, 'w') as f:
f.write(_NEW_CONTENTS)
self.device.PushChangedFiles([(host_file_path, device_file_path)])
result = self.device.RunShellCommand(
['cat', device_file_path], check_return=True, single_line=True)
self.assertEqual(_NEW_CONTENTS, result)
cmd_helper.RunCmd(['rm', host_file_path])
self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testDeleteFiles(self):
host_tmp_dir = tempfile.mkdtemp()
(host_file_path, file_name) = self._MakeTempFileGivenDir(
host_tmp_dir, _OLD_CONTENTS)
device_file_path = "%s/%s" % (_DEVICE_DIR, file_name)
self.adb.Push(host_file_path, device_file_path)
cmd_helper.RunCmd(['rm', host_file_path])
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
filenames = self.device.ListDirectory(_DEVICE_DIR)
self.assertEqual([], filenames)
cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testPushAndDeleteFiles_noSubDir(self):
host_tmp_dir = tempfile.mkdtemp()
(host_file_path1, file_name1) = self._MakeTempFileGivenDir(
host_tmp_dir, _OLD_CONTENTS)
(host_file_path2, file_name2) = self._MakeTempFileGivenDir(
host_tmp_dir, _OLD_CONTENTS)
device_file_path1 = "%s/%s" % (_DEVICE_DIR, file_name1)
device_file_path2 = "%s/%s" % (_DEVICE_DIR, file_name2)
self.adb.Push(host_file_path1, device_file_path1)
self.adb.Push(host_file_path2, device_file_path2)
with open(host_file_path1, 'w') as f:
f.write(_NEW_CONTENTS)
cmd_helper.RunCmd(['rm', host_file_path2])
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
result = self.device.RunShellCommand(
['cat', device_file_path1], check_return=True, single_line=True)
self.assertEqual(_NEW_CONTENTS, result)
filenames = self.device.ListDirectory(_DEVICE_DIR)
self.assertEqual([file_name1], filenames)
cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testPushAndDeleteFiles_SubDir(self):
host_tmp_dir = tempfile.mkdtemp()
host_sub_dir1 = "%s/%s" % (host_tmp_dir, _SUB_DIR1)
host_sub_dir2 = "%s/%s/%s" % (host_tmp_dir, _SUB_DIR, _SUB_DIR2)
cmd_helper.RunCmd(['mkdir', '-p', host_sub_dir1])
cmd_helper.RunCmd(['mkdir', '-p', host_sub_dir2])
(host_file_path1, file_name1) = self._MakeTempFileGivenDir(
host_tmp_dir, _OLD_CONTENTS)
(host_file_path2, file_name2) = self._MakeTempFileGivenDir(
host_tmp_dir, _OLD_CONTENTS)
(host_file_path3, file_name3) = self._MakeTempFileGivenDir(
host_sub_dir1, _OLD_CONTENTS)
(host_file_path4, file_name4) = self._MakeTempFileGivenDir(
host_sub_dir2, _OLD_CONTENTS)
device_file_path1 = "%s/%s" % (_DEVICE_DIR, file_name1)
device_file_path2 = "%s/%s" % (_DEVICE_DIR, file_name2)
device_file_path3 = "%s/%s/%s" % (_DEVICE_DIR, _SUB_DIR1, file_name3)
device_file_path4 = "%s/%s/%s/%s" % (_DEVICE_DIR, _SUB_DIR,
_SUB_DIR2, file_name4)
self.adb.Push(host_file_path1, device_file_path1)
self.adb.Push(host_file_path2, device_file_path2)
self.adb.Push(host_file_path3, device_file_path3)
self.adb.Push(host_file_path4, device_file_path4)
with open(host_file_path1, 'w') as f:
f.write(_NEW_CONTENTS)
cmd_helper.RunCmd(['rm', host_file_path2])
cmd_helper.RunCmd(['rm', host_file_path4])
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
result = self.device.RunShellCommand(
['cat', device_file_path1], check_return=True, single_line=True)
self.assertEqual(_NEW_CONTENTS, result)
filenames = self.device.ListDirectory(_DEVICE_DIR)
self.assertIn(file_name1, filenames)
self.assertIn(_SUB_DIR1, filenames)
self.assertIn(_SUB_DIR, filenames)
self.assertEqual(3, len(filenames))
result = self.device.RunShellCommand(
['cat', device_file_path3], check_return=True, single_line=True)
self.assertEqual(_OLD_CONTENTS, result)
filenames = self.device.ListDirectory(
posixpath.join(_DEVICE_DIR, _SUB_DIR, _SUB_DIR2))
self.assertEqual([], filenames)
cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testPushWithStaleDirectories(self):
# Make a few files and directories to push.
host_tmp_dir = tempfile.mkdtemp()
host_sub_dir1 = '%s/%s' % (host_tmp_dir, _SUB_DIR1)
host_sub_dir2 = "%s/%s/%s" % (host_tmp_dir, _SUB_DIR, _SUB_DIR2)
os.makedirs(host_sub_dir1)
os.makedirs(host_sub_dir2)
self._MakeTempFileGivenDir(host_sub_dir1, _OLD_CONTENTS)
self._MakeTempFileGivenDir(host_sub_dir2, _OLD_CONTENTS)
# Push all our created files/directories and verify they're on the device.
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
top_level_dirs = self.device.ListDirectory(_DEVICE_DIR)
self.assertIn(_SUB_DIR1, top_level_dirs)
self.assertIn(_SUB_DIR, top_level_dirs)
sub_dir = self.device.ListDirectory('%s/%s' % (_DEVICE_DIR, _SUB_DIR))
self.assertIn(_SUB_DIR2, sub_dir)
# Remove one of the directories on the host and push again.
cmd_helper.RunCmd(['rm', '-rf', host_sub_dir2])
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
# Verify that the directory we removed is no longer on the device, but the
# other directories still are.
top_level_dirs = self.device.ListDirectory(_DEVICE_DIR)
self.assertIn(_SUB_DIR1, top_level_dirs)
self.assertIn(_SUB_DIR, top_level_dirs)
sub_dir = self.device.ListDirectory('%s/%s' % (_DEVICE_DIR, _SUB_DIR))
self.assertEqual([], sub_dir)
def testRestartAdbd(self):
def get_adbd_pid():
try:
return next(p.pid for p in self.device.ListProcesses('adbd'))
except StopIteration:
self.fail('Unable to find adbd')
old_adbd_pid = get_adbd_pid()
self.device.RestartAdbd()
new_adbd_pid = get_adbd_pid()
self.assertNotEqual(old_adbd_pid, new_adbd_pid)
def testEnableRoot(self):
self.device.SetProp('service.adb.root', '0')
self.device.RestartAdbd()
self.assertFalse(self.device.HasRoot())
self.assertIn(self.device.GetProp('service.adb.root'), ('', '0'))
self.device.EnableRoot()
self.assertTrue(self.device.HasRoot())
self.assertEquals(self.device.GetProp('service.adb.root'), '1')
class PsOutputCompatibilityTests(device_test_case.DeviceTestCase):
def setUp(self):
super(PsOutputCompatibilityTests, self).setUp()
self.adb = adb_wrapper.AdbWrapper(self.serial)
self.adb.WaitForDevice()
self.device = device_utils.DeviceUtils(self.adb, default_retries=0)
def testPsOutoutCompatibility(self):
# pylint: disable=protected-access
lines = self.device._GetPsOutput(None)
# Check column names at each index match expected values.
header = lines[0].split()
for column, idx in device_utils._PS_COLUMNS.iteritems():
column = column.upper()
self.assertEqual(
header[idx], column,
'Expected column %s at index %d but found %s\nsource: %r' % (
column, idx, header[idx], lines[0]))
# Check pid and ppid are numeric values.
for line in lines[1:]:
row = line.split()
row = {k: row[i] for k, i in device_utils._PS_COLUMNS.iteritems()}
for key in ('pid', 'ppid'):
self.assertTrue(
row[key].isdigit(),
'Expected numeric %s value but found %r\nsource: %r' % (
key, row[key], line))
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,256 @@
# 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 based on fastboot."""
# pylint: disable=unused-argument
import collections
import contextlib
import fnmatch
import logging
import os
import re
from devil.android import decorators
from devil.android import device_errors
from devil.android.sdk import fastboot
from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3
_FASTBOOT_REBOOT_TIMEOUT = 10 * _DEFAULT_TIMEOUT
_KNOWN_PARTITIONS = collections.OrderedDict([
('bootloader', {'image': 'bootloader*.img', 'restart': True}),
('radio', {'image': 'radio*.img', 'restart': True}),
('boot', {'image': 'boot.img'}),
('recovery', {'image': 'recovery.img'}),
('system', {'image': 'system.img'}),
('userdata', {'image': 'userdata.img', 'wipe_only': True}),
('cache', {'image': 'cache.img', 'wipe_only': True}),
('vendor', {'image': 'vendor*.img', 'optional': True}),
])
ALL_PARTITIONS = _KNOWN_PARTITIONS.keys()
def _FindAndVerifyPartitionsAndImages(partitions, directory):
"""Validate partitions and images.
Validate all partition names and partition directories. Cannot stop mid
flash so its important to validate everything first.
Args:
Partitions: partitions to be tested.
directory: directory containing the images.
Returns:
Dictionary with exact partition, image name mapping.
"""
files = os.listdir(directory)
return_dict = collections.OrderedDict()
def find_file(pattern):
for filename in files:
if fnmatch.fnmatch(filename, pattern):
return os.path.join(directory, filename)
return None
for partition in partitions:
partition_info = _KNOWN_PARTITIONS[partition]
image_file = find_file(partition_info['image'])
if image_file:
return_dict[partition] = image_file
elif not partition_info.get('optional'):
raise device_errors.FastbootCommandFailedError(
'Failed to flash device. Could not find image for %s.',
partition_info['image'])
return return_dict
class FastbootUtils(object):
_FASTBOOT_WAIT_TIME = 1
_BOARD_VERIFICATION_FILE = 'android-info.txt'
def __init__(self, device, fastbooter=None, default_timeout=_DEFAULT_TIMEOUT,
default_retries=_DEFAULT_RETRIES):
"""FastbootUtils constructor.
Example Usage to flash a device:
fastboot = fastboot_utils.FastbootUtils(device)
fastboot.FlashDevice('/path/to/build/directory')
Args:
device: A DeviceUtils instance.
fastbooter: Optional fastboot object. If none is passed, one will
be created.
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.
"""
self._device = device
self._board = device.product_board
self._serial = str(device)
self._default_timeout = default_timeout
self._default_retries = default_retries
if fastbooter:
self.fastboot = fastbooter
else:
self.fastboot = fastboot.Fastboot(self._serial)
@decorators.WithTimeoutAndRetriesFromInstance()
def WaitForFastbootMode(self, timeout=None, retries=None):
"""Wait for device to boot into fastboot mode.
This waits for the device serial to show up in fastboot devices output.
"""
def fastboot_mode():
return any(self._serial == str(d) for d in self.fastboot.Devices())
timeout_retry.WaitFor(fastboot_mode, wait_period=self._FASTBOOT_WAIT_TIME)
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=_FASTBOOT_REBOOT_TIMEOUT)
def EnableFastbootMode(self, timeout=None, retries=None):
"""Reboots phone into fastboot mode.
Roots phone if needed, then reboots phone into fastboot mode and waits.
"""
self._device.EnableRoot()
self._device.adb.Reboot(to_bootloader=True)
self.WaitForFastbootMode()
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=_FASTBOOT_REBOOT_TIMEOUT)
def Reboot(
self, bootloader=False, wait_for_reboot=True, timeout=None, retries=None):
"""Reboots out of fastboot mode.
It reboots the phone either back into fastboot, or to a regular boot. It
then blocks until the device is ready.
Args:
bootloader: If set to True, reboots back into bootloader.
"""
if bootloader:
self.fastboot.RebootBootloader()
self.WaitForFastbootMode()
else:
self.fastboot.Reboot()
if wait_for_reboot:
self._device.WaitUntilFullyBooted(timeout=_FASTBOOT_REBOOT_TIMEOUT)
def _VerifyBoard(self, directory):
"""Validate as best as possible that the android build matches the device.
Goes through build files and checks if the board name is mentioned in the
|self._BOARD_VERIFICATION_FILE| or in the build archive.
Args:
directory: directory where build files are located.
"""
files = os.listdir(directory)
board_regex = re.compile(r'require board=(\w+)')
if self._BOARD_VERIFICATION_FILE in files:
with open(os.path.join(directory, self._BOARD_VERIFICATION_FILE)) as f:
for line in f:
m = board_regex.match(line)
if m:
board_name = m.group(1)
if board_name == self._board:
return True
elif board_name:
return False
else:
logger.warning('No board type found in %s.',
self._BOARD_VERIFICATION_FILE)
else:
logger.warning('%s not found. Unable to use it to verify device.',
self._BOARD_VERIFICATION_FILE)
zip_regex = re.compile(r'.*%s.*\.zip' % re.escape(self._board))
for f in files:
if zip_regex.match(f):
return True
return False
def _FlashPartitions(self, partitions, directory, wipe=False, force=False):
"""Flashes all given partiitons with all given images.
Args:
partitions: List of partitions to flash.
directory: Directory where all partitions can be found.
wipe: If set to true, will automatically detect if cache and userdata
partitions are sent, and if so ignore them.
force: boolean to decide to ignore board name safety checks.
Raises:
device_errors.CommandFailedError(): If image cannot be found or if bad
partition name is give.
"""
if not self._VerifyBoard(directory):
if force:
logger.warning('Could not verify build is meant to be installed on '
'the current device type, but force flag is set. '
'Flashing device. Possibly dangerous operation.')
else:
raise device_errors.CommandFailedError(
'Could not verify build is meant to be installed on the current '
'device type. Run again with force=True to force flashing with an '
'unverified board.')
flash_image_files = _FindAndVerifyPartitionsAndImages(partitions, directory)
partitions = flash_image_files.keys()
for partition in partitions:
if _KNOWN_PARTITIONS[partition].get('wipe_only') and not wipe:
logger.info(
'Not flashing in wipe mode. Skipping partition %s.', partition)
else:
logger.info(
'Flashing %s with %s', partition, flash_image_files[partition])
self.fastboot.Flash(partition, flash_image_files[partition])
if _KNOWN_PARTITIONS[partition].get('restart', False):
self.Reboot(bootloader=True)
@contextlib.contextmanager
def FastbootMode(self, wait_for_reboot=True, timeout=None, retries=None):
"""Context manager that enables fastboot mode, and reboots after.
Example usage:
with FastbootMode():
Flash Device
# Anything that runs after flashing.
"""
self.EnableFastbootMode()
self.fastboot.SetOemOffModeCharge(False)
try:
yield self
finally:
self.fastboot.SetOemOffModeCharge(True)
self.Reboot(wait_for_reboot=wait_for_reboot)
def FlashDevice(self, directory, partitions=None, wipe=False):
"""Flash device with build in |directory|.
Directory must contain bootloader, radio, boot, recovery, system, userdata,
and cache .img files from an android build. This is a dangerous operation so
use with care.
Args:
fastboot: A FastbootUtils instance.
directory: Directory with build files.
wipe: Wipes cache and userdata if set to true.
partitions: List of partitions to flash. Defaults to all.
"""
if partitions is None:
partitions = ALL_PARTITIONS
# If a device is wiped, then it will no longer have adb keys so it cannot be
# communicated with to verify that it is rebooted. It is up to the user of
# this script to ensure that the adb keys are set on the device after using
# this to wipe a device.
with self.FastbootMode(wait_for_reboot=not wipe):
self._FlashPartitions(partitions, directory, wipe=wipe)

View File

@@ -0,0 +1,375 @@
#!/usr/bin/env python
# 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.
"""
Unit tests for the contents of fastboot_utils.py
"""
# pylint: disable=protected-access,unused-argument
import collections
import io
import logging
import unittest
from devil import devil_env
from devil.android import device_errors
from devil.android import device_utils
from devil.android import fastboot_utils
from devil.android.sdk import fastboot
from devil.utils import mock_calls
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
_BOARD = 'board_type'
_SERIAL = '0123456789abcdef'
_PARTITIONS = [
'bootloader', 'radio', 'boot', 'recovery', 'system', 'userdata', 'cache']
_IMAGES = collections.OrderedDict([
('bootloader', 'bootloader.img'),
('radio', 'radio.img'),
('boot', 'boot.img'),
('recovery', 'recovery.img'),
('system', 'system.img'),
('userdata', 'userdata.img'),
('cache', 'cache.img')
])
_VALID_FILES = [_BOARD + '.zip', 'android-info.txt']
_INVALID_FILES = ['test.zip', 'android-info.txt']
class MockFile(object):
def __init__(self, name='/tmp/some/file'):
self.file = mock.MagicMock(spec=file)
self.file.name = name
def __enter__(self):
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
pass
@property
def name(self):
return self.file.name
def _FastbootWrapperMock(test_serial):
fastbooter = mock.Mock(spec=fastboot.Fastboot)
fastbooter.__str__ = mock.Mock(return_value=test_serial)
fastbooter.Devices.return_value = [test_serial]
return fastbooter
def _DeviceUtilsMock(test_serial):
device = mock.Mock(spec=device_utils.DeviceUtils)
device.__str__ = mock.Mock(return_value=test_serial)
device.product_board = mock.Mock(return_value=_BOARD)
device.adb = mock.Mock()
return device
class FastbootUtilsTest(mock_calls.TestCase):
def setUp(self):
self.device_utils_mock = _DeviceUtilsMock(_SERIAL)
self.fastboot_wrapper = _FastbootWrapperMock(_SERIAL)
self.fastboot = fastboot_utils.FastbootUtils(
self.device_utils_mock, fastbooter=self.fastboot_wrapper,
default_timeout=2, default_retries=0)
self.fastboot._board = _BOARD
class FastbootUtilsInitTest(FastbootUtilsTest):
def testInitWithDeviceUtil(self):
f = fastboot_utils.FastbootUtils(self.device_utils_mock)
self.assertEqual(str(self.device_utils_mock), str(f._device))
def testInitWithMissing_fails(self):
with self.assertRaises(AttributeError):
fastboot_utils.FastbootUtils(None)
with self.assertRaises(AttributeError):
fastboot_utils.FastbootUtils('')
def testPartitionOrdering(self):
parts = ['bootloader', 'radio', 'boot', 'recovery', 'system', 'userdata',
'cache', 'vendor']
self.assertListEqual(fastboot_utils.ALL_PARTITIONS, parts)
class FastbootUtilsWaitForFastbootMode(FastbootUtilsTest):
# If this test fails by timing out after 1 second.
@mock.patch('time.sleep', mock.Mock())
def testWaitForFastbootMode(self):
self.fastboot.WaitForFastbootMode()
class FastbootUtilsEnableFastbootMode(FastbootUtilsTest):
def testEnableFastbootMode(self):
with self.assertCalls(
self.call.fastboot._device.EnableRoot(),
self.call.fastboot._device.adb.Reboot(to_bootloader=True),
self.call.fastboot.WaitForFastbootMode()):
self.fastboot.EnableFastbootMode()
class FastbootUtilsReboot(FastbootUtilsTest):
def testReboot_bootloader(self):
with self.assertCalls(
self.call.fastboot.fastboot.RebootBootloader(),
self.call.fastboot.WaitForFastbootMode()):
self.fastboot.Reboot(bootloader=True)
def testReboot_normal(self):
with self.assertCalls(
self.call.fastboot.fastboot.Reboot(),
self.call.fastboot._device.WaitUntilFullyBooted(timeout=mock.ANY)):
self.fastboot.Reboot()
class FastbootUtilsFlashPartitions(FastbootUtilsTest):
def testFlashPartitions_wipe(self):
with self.assertCalls(
(self.call.fastboot._VerifyBoard('test'), True),
(mock.call.devil.android.fastboot_utils.
_FindAndVerifyPartitionsAndImages(_PARTITIONS, 'test'), _IMAGES),
(self.call.fastboot.fastboot.Flash('bootloader', 'bootloader.img')),
(self.call.fastboot.Reboot(bootloader=True)),
(self.call.fastboot.fastboot.Flash('radio', 'radio.img')),
(self.call.fastboot.Reboot(bootloader=True)),
(self.call.fastboot.fastboot.Flash('boot', 'boot.img')),
(self.call.fastboot.fastboot.Flash('recovery', 'recovery.img')),
(self.call.fastboot.fastboot.Flash('system', 'system.img')),
(self.call.fastboot.fastboot.Flash('userdata', 'userdata.img')),
(self.call.fastboot.fastboot.Flash('cache', 'cache.img'))):
self.fastboot._FlashPartitions(_PARTITIONS, 'test', wipe=True)
def testFlashPartitions_noWipe(self):
with self.assertCalls(
(self.call.fastboot._VerifyBoard('test'), True),
(mock.call.devil.android.fastboot_utils.
_FindAndVerifyPartitionsAndImages(_PARTITIONS, 'test'), _IMAGES),
(self.call.fastboot.fastboot.Flash('bootloader', 'bootloader.img')),
(self.call.fastboot.Reboot(bootloader=True)),
(self.call.fastboot.fastboot.Flash('radio', 'radio.img')),
(self.call.fastboot.Reboot(bootloader=True)),
(self.call.fastboot.fastboot.Flash('boot', 'boot.img')),
(self.call.fastboot.fastboot.Flash('recovery', 'recovery.img')),
(self.call.fastboot.fastboot.Flash('system', 'system.img'))):
self.fastboot._FlashPartitions(_PARTITIONS, 'test')
class FastbootUtilsFastbootMode(FastbootUtilsTest):
def testFastbootMode_goodWait(self):
with self.assertCalls(
self.call.fastboot.EnableFastbootMode(),
self.call.fastboot.fastboot.SetOemOffModeCharge(False),
self.call.fastboot.fastboot.SetOemOffModeCharge(True),
self.call.fastboot.Reboot(wait_for_reboot=True)):
with self.fastboot.FastbootMode() as fbm:
self.assertEqual(self.fastboot, fbm)
def testFastbootMode_goodNoWait(self):
with self.assertCalls(
self.call.fastboot.EnableFastbootMode(),
self.call.fastboot.fastboot.SetOemOffModeCharge(False),
self.call.fastboot.fastboot.SetOemOffModeCharge(True),
self.call.fastboot.Reboot(wait_for_reboot=False)):
with self.fastboot.FastbootMode(wait_for_reboot=False) as fbm:
self.assertEqual(self.fastboot, fbm)
def testFastbootMode_exception(self):
with self.assertCalls(
self.call.fastboot.EnableFastbootMode(),
self.call.fastboot.fastboot.SetOemOffModeCharge(False),
self.call.fastboot.fastboot.SetOemOffModeCharge(True),
self.call.fastboot.Reboot(wait_for_reboot=True)):
with self.assertRaises(NotImplementedError):
with self.fastboot.FastbootMode() as fbm:
self.assertEqual(self.fastboot, fbm)
raise NotImplementedError
def testFastbootMode_exceptionInEnableFastboot(self):
self.fastboot.EnableFastbootMode = mock.Mock()
self.fastboot.EnableFastbootMode.side_effect = NotImplementedError
with self.assertRaises(NotImplementedError):
with self.fastboot.FastbootMode():
pass
class FastbootUtilsVerifyBoard(FastbootUtilsTest):
def testVerifyBoard_bothValid(self):
mock_file = io.StringIO(u'require board=%s\n' % _BOARD)
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=_VALID_FILES):
self.assertTrue(self.fastboot._VerifyBoard('test'))
def testVerifyBoard_BothNotValid(self):
mock_file = io.StringIO(u'abc')
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=_INVALID_FILES):
self.assertFalse(self.assertFalse(self.fastboot._VerifyBoard('test')))
def testVerifyBoard_FileNotFoundZipValid(self):
with mock.patch('os.listdir', return_value=[_BOARD + '.zip']):
self.assertTrue(self.fastboot._VerifyBoard('test'))
def testVerifyBoard_ZipNotFoundFileValid(self):
mock_file = io.StringIO(u'require board=%s\n' % _BOARD)
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=['android-info.txt']):
self.assertTrue(self.fastboot._VerifyBoard('test'))
def testVerifyBoard_zipNotValidFileIs(self):
mock_file = io.StringIO(u'require board=%s\n' % _BOARD)
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=_INVALID_FILES):
self.assertTrue(self.fastboot._VerifyBoard('test'))
def testVerifyBoard_fileNotValidZipIs(self):
mock_file = io.StringIO(u'require board=WrongBoard')
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=_VALID_FILES):
self.assertFalse(self.fastboot._VerifyBoard('test'))
def testVerifyBoard_noBoardInFileValidZip(self):
mock_file = io.StringIO(u'Regex wont match')
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=_VALID_FILES):
self.assertTrue(self.fastboot._VerifyBoard('test'))
def testVerifyBoard_noBoardInFileInvalidZip(self):
mock_file = io.StringIO(u'Regex wont match')
with mock.patch('__builtin__.open', return_value=mock_file, create=True):
with mock.patch('os.listdir', return_value=_INVALID_FILES):
self.assertFalse(self.fastboot._VerifyBoard('test'))
class FastbootUtilsFindAndVerifyPartitionsAndImages(FastbootUtilsTest):
def testFindAndVerifyPartitionsAndImages_validNoVendor(self):
PARTITIONS = [
'bootloader', 'radio', 'boot', 'recovery', 'system', 'userdata',
'cache', 'vendor'
]
files = [
'bootloader-test-.img',
'radio123.img',
'boot.img',
'recovery.img',
'system.img',
'userdata.img',
'cache.img'
]
img_check = collections.OrderedDict([
('bootloader', 'test/bootloader-test-.img'),
('radio', 'test/radio123.img'),
('boot', 'test/boot.img'),
('recovery', 'test/recovery.img'),
('system', 'test/system.img'),
('userdata', 'test/userdata.img'),
('cache', 'test/cache.img'),
])
parts_check = [
'bootloader', 'radio', 'boot', 'recovery', 'system', 'userdata',
'cache'
]
with mock.patch('os.listdir', return_value=files):
imgs = fastboot_utils._FindAndVerifyPartitionsAndImages(
PARTITIONS, 'test')
parts = imgs.keys()
self.assertDictEqual(imgs, img_check)
self.assertListEqual(parts, parts_check)
def testFindAndVerifyPartitionsAndImages_validVendor(self):
PARTITIONS = [
'bootloader', 'radio', 'boot', 'recovery', 'system', 'userdata',
'cache', 'vendor'
]
files = [
'bootloader-test-.img',
'radio123.img',
'boot.img',
'recovery.img',
'system.img',
'userdata.img',
'cache.img',
'vendor.img'
]
img_check = {
'bootloader': 'test/bootloader-test-.img',
'radio': 'test/radio123.img',
'boot': 'test/boot.img',
'recovery': 'test/recovery.img',
'system': 'test/system.img',
'userdata': 'test/userdata.img',
'cache': 'test/cache.img',
'vendor': 'test/vendor.img',
}
parts_check = [
'bootloader', 'radio', 'boot', 'recovery', 'system', 'userdata',
'cache', 'vendor'
]
with mock.patch('os.listdir', return_value=files):
imgs = fastboot_utils._FindAndVerifyPartitionsAndImages(
PARTITIONS, 'test')
parts = imgs.keys()
self.assertDictEqual(imgs, img_check)
self.assertListEqual(parts, parts_check)
def testFindAndVerifyPartitionsAndImages_badPartition(self):
with mock.patch('os.listdir', return_value=['test']):
with self.assertRaises(KeyError):
fastboot_utils._FindAndVerifyPartitionsAndImages(['test'], 'test')
def testFindAndVerifyPartitionsAndImages_noFile(self):
with mock.patch('os.listdir', return_value=['test']):
with self.assertRaises(device_errors.FastbootCommandFailedError):
fastboot_utils._FindAndVerifyPartitionsAndImages(['cache'], 'test')
class FastbootUtilsFlashDevice(FastbootUtilsTest):
def testFlashDevice_wipe(self):
with self.assertCalls(
self.call.fastboot.EnableFastbootMode(),
self.call.fastboot.fastboot.SetOemOffModeCharge(False),
self.call.fastboot._FlashPartitions(mock.ANY, 'test', wipe=True),
self.call.fastboot.fastboot.SetOemOffModeCharge(True),
self.call.fastboot.Reboot(wait_for_reboot=False)):
self.fastboot.FlashDevice('test', wipe=True)
def testFlashDevice_noWipe(self):
with self.assertCalls(
self.call.fastboot.EnableFastbootMode(),
self.call.fastboot.fastboot.SetOemOffModeCharge(False),
self.call.fastboot._FlashPartitions(mock.ANY, 'test', wipe=False),
self.call.fastboot.fastboot.SetOemOffModeCharge(True),
self.call.fastboot.Reboot(wait_for_reboot=True)):
self.fastboot.FlashDevice('test', wipe=False)
def testFlashDevice_partitions(self):
with self.assertCalls(
self.call.fastboot.EnableFastbootMode(),
self.call.fastboot.fastboot.SetOemOffModeCharge(False),
self.call.fastboot._FlashPartitions(['boot'], 'test', wipe=False),
self.call.fastboot.fastboot.SetOemOffModeCharge(True),
self.call.fastboot.Reboot(wait_for_reboot=True)):
self.fastboot.FlashDevice('test', partitions=['boot'], wipe=False)
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)

View File

@@ -0,0 +1,328 @@
# 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.
import contextlib
import logging
import posixpath
import re
from devil.android.sdk import version_codes
logger = logging.getLogger(__name__)
_CMDLINE_DIR = '/data/local/tmp'
_CMDLINE_DIR_LEGACY = '/data/local'
_RE_NEEDS_QUOTING = re.compile(r'[^\w-]') # Not in: alphanumeric or hyphens.
_QUOTES = '"\'' # Either a single or a double quote.
_ESCAPE = '\\' # A backslash.
@contextlib.contextmanager
def CustomCommandLineFlags(device, cmdline_name, flags):
"""Context manager to change Chrome's command line temporarily.
Example:
with flag_changer.TemporaryCommandLineFlags(device, name, flags):
# Launching Chrome will use the provided flags.
# Previous set of flags on the device is now restored.
Args:
device: A DeviceUtils instance.
cmdline_name: Name of the command line file where to store flags.
flags: A sequence of command line flags to set.
"""
changer = FlagChanger(device, cmdline_name)
try:
changer.ReplaceFlags(flags)
yield
finally:
changer.Restore()
class FlagChanger(object):
"""Changes the flags Chrome runs with.
Flags can be temporarily set for a particular set of unit tests. These
tests should call Restore() to revert the flags to their original state
once the tests have completed.
"""
def __init__(self, device, cmdline_file, use_legacy_path=False):
"""Initializes the FlagChanger and records the original arguments.
Args:
device: A DeviceUtils instance.
cmdline_file: Name of the command line file where to store flags.
use_legacy_path: Whether to use the legacy commandline path (needed for
M54 and earlier)
"""
self._device = device
self._should_reset_enforce = False
if posixpath.sep in cmdline_file:
raise ValueError(
'cmdline_file should be a file name only, do not include path'
' separators in: %s' % cmdline_file)
cmdline_path = posixpath.join(_CMDLINE_DIR, cmdline_file)
alternate_cmdline_path = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file)
if use_legacy_path:
cmdline_path, alternate_cmdline_path = (
alternate_cmdline_path, cmdline_path)
if not self._device.HasRoot():
raise ValueError('use_legacy_path requires a rooted device')
self._cmdline_path = cmdline_path
if self._device.PathExists(alternate_cmdline_path):
logger.warning(
'Removing alternate command line file %r.', alternate_cmdline_path)
self._device.RemovePath(alternate_cmdline_path, as_root=True)
self._state_stack = [None] # Actual state is set by GetCurrentFlags().
self.GetCurrentFlags()
def GetCurrentFlags(self):
"""Read the current flags currently stored in the device.
Also updates the internal state of the flag_changer.
Returns:
A list of flags.
"""
if self._device.PathExists(self._cmdline_path):
command_line = self._device.ReadFile(
self._cmdline_path, as_root=True).strip()
else:
command_line = ''
flags = _ParseFlags(command_line)
# Store the flags as a set to facilitate adding and removing flags.
self._state_stack[-1] = set(flags)
return flags
def ReplaceFlags(self, flags, log_flags=True):
"""Replaces the flags in the command line with the ones provided.
Saves the current flags state on the stack, so a call to Restore will
change the state back to the one preceeding the call to ReplaceFlags.
Args:
flags: A sequence of command line flags to set, eg. ['--single-process'].
Note: this should include flags only, not the name of a command
to run (ie. there is no need to start the sequence with 'chrome').
Returns:
A list with the flags now stored on the device.
"""
new_flags = set(flags)
self._state_stack.append(new_flags)
self._SetPermissive()
return self._UpdateCommandLineFile(log_flags=log_flags)
def AddFlags(self, flags):
"""Appends flags to the command line if they aren't already there.
Saves the current flags state on the stack, so a call to Restore will
change the state back to the one preceeding the call to AddFlags.
Args:
flags: A sequence of flags to add on, eg. ['--single-process'].
Returns:
A list with the flags now stored on the device.
"""
return self.PushFlags(add=flags)
def RemoveFlags(self, flags):
"""Removes flags from the command line, if they exist.
Saves the current flags state on the stack, so a call to Restore will
change the state back to the one preceeding the call to RemoveFlags.
Note that calling RemoveFlags after AddFlags will result in having
two nested states.
Args:
flags: A sequence of flags to remove, eg. ['--single-process']. Note
that we expect a complete match when removing flags; if you want
to remove a switch with a value, you must use the exact string
used to add it in the first place.
Returns:
A list with the flags now stored on the device.
"""
return self.PushFlags(remove=flags)
def PushFlags(self, add=None, remove=None):
"""Appends and removes flags to/from the command line if they aren't already
there. Saves the current flags state on the stack, so a call to Restore
will change the state back to the one preceeding the call to PushFlags.
Args:
add: A list of flags to add on, eg. ['--single-process'].
remove: A list of flags to remove, eg. ['--single-process']. Note that we
expect a complete match when removing flags; if you want to remove
a switch with a value, you must use the exact string used to add
it in the first place.
Returns:
A list with the flags now stored on the device.
"""
new_flags = self._state_stack[-1].copy()
if add:
new_flags.update(add)
if remove:
new_flags.difference_update(remove)
return self.ReplaceFlags(new_flags)
def _SetPermissive(self):
"""Set SELinux to permissive, if needed.
On Android N and above this is needed in order to allow Chrome to read the
legacy command line file.
TODO(crbug.com/699082): Remove when a better solution exists.
"""
# TODO(crbug.com/948578): figure out the exact scenarios where the lowered
# permissions are needed, and document them in the code.
if not self._device.HasRoot():
return
if (self._device.build_version_sdk >= version_codes.NOUGAT and
self._device.GetEnforce()):
self._device.SetEnforce(enabled=False)
self._should_reset_enforce = True
def _ResetEnforce(self):
"""Restore SELinux policy if it had been previously made permissive."""
if self._should_reset_enforce:
self._device.SetEnforce(enabled=True)
self._should_reset_enforce = False
def Restore(self):
"""Restores the flags to their state prior to the last AddFlags or
RemoveFlags call.
Returns:
A list with the flags now stored on the device.
"""
# The initial state must always remain on the stack.
assert len(self._state_stack) > 1, (
'Mismatch between calls to Add/RemoveFlags and Restore')
self._state_stack.pop()
if len(self._state_stack) == 1:
self._ResetEnforce()
return self._UpdateCommandLineFile()
def _UpdateCommandLineFile(self, log_flags=True):
"""Writes out the command line to the file, or removes it if empty.
Returns:
A list with the flags now stored on the device.
"""
command_line = _SerializeFlags(self._state_stack[-1])
if command_line is not None:
self._device.WriteFile(self._cmdline_path, command_line, as_root=True)
else:
self._device.RemovePath(self._cmdline_path, force=True, as_root=True)
flags = self.GetCurrentFlags()
logging.info('Flags now written on the device to %s', self._cmdline_path)
if log_flags:
logging.info('Flags: %s', flags)
return flags
def _ParseFlags(line):
"""Parse the string containing the command line into a list of flags.
It's a direct port of CommandLine.java::tokenizeQuotedArguments.
The first token is assumed to be the (unused) program name and stripped off
from the list of flags.
Args:
line: A string containing the entire command line. The first token is
assumed to be the program name.
Returns:
A list of flags, with quoting removed.
"""
flags = []
current_quote = None
current_flag = None
# pylint: disable=unsubscriptable-object
for c in line:
# Detect start or end of quote block.
if (current_quote is None and c in _QUOTES) or c == current_quote:
if current_flag is not None and current_flag[-1] == _ESCAPE:
# Last char was a backslash; pop it, and treat c as a literal.
current_flag = current_flag[:-1] + c
else:
current_quote = c if current_quote is None else None
elif current_quote is None and c.isspace():
if current_flag is not None:
flags.append(current_flag)
current_flag = None
else:
if current_flag is None:
current_flag = ''
current_flag += c
if current_flag is not None:
if current_quote is not None:
logger.warning('Unterminated quoted argument: ' + current_flag)
flags.append(current_flag)
# Return everything but the program name.
return flags[1:]
def _SerializeFlags(flags):
"""Serialize a sequence of flags into a command line string.
Args:
flags: A sequence of strings with individual flags.
Returns:
A line with the command line contents to save; or None if the sequence of
flags is empty.
"""
if flags:
# The first command line argument doesn't matter as we are not actually
# launching the chrome executable using this command line.
args = ['_']
args.extend(_QuoteFlag(f) for f in flags)
return ' '.join(args)
else:
return None
def _QuoteFlag(flag):
"""Validate and quote a single flag.
Args:
A string with the flag to quote.
Returns:
A string with the flag quoted so that it can be parsed by the algorithm
in _ParseFlags; or None if the flag does not appear to be valid.
"""
if '=' in flag:
key, value = flag.split('=', 1)
else:
key, value = flag, None
if not flag or _RE_NEEDS_QUOTING.search(key):
# Probably not a valid flag, but quote the whole thing so it can be
# parsed back correctly.
return '"%s"' % flag.replace('"', r'\"')
if value is None:
return key
if _RE_NEEDS_QUOTING.search(value):
value = '"%s"' % value.replace('"', r'\"')
return '='.join([key, value])

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python
# Copyright 2017 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.
"""
Unit tests for the contents of flag_changer.py.
The test will invoke real devices
"""
import os
import posixpath
import sys
import unittest
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', )))
from devil.android import device_test_case
from devil.android import device_utils
from devil.android import flag_changer
from devil.android.sdk import adb_wrapper
_CMDLINE_FILE = 'dummy-command-line'
class FlagChangerTest(device_test_case.DeviceTestCase):
def setUp(self):
super(FlagChangerTest, self).setUp()
self.adb = adb_wrapper.AdbWrapper(self.serial)
self.adb.WaitForDevice()
self.device = device_utils.DeviceUtils(
self.adb, default_timeout=10, default_retries=0)
# pylint: disable=protected-access
self.cmdline_path = posixpath.join(flag_changer._CMDLINE_DIR, _CMDLINE_FILE)
self.cmdline_path_legacy = posixpath.join(
flag_changer._CMDLINE_DIR_LEGACY, _CMDLINE_FILE)
def tearDown(self):
super(FlagChangerTest, self).tearDown()
self.device.RemovePath(
[self.cmdline_path, self.cmdline_path_legacy], force=True, as_root=True)
def testFlagChanger_restoreFlags(self):
if not self.device.HasRoot():
self.skipTest('Test needs a rooted device')
# Write some custom chrome command line flags.
self.device.WriteFile(
self.cmdline_path, 'chrome --some --old --flags')
# Write some more flags on a command line file in the legacy location.
self.device.WriteFile(
self.cmdline_path_legacy, 'some --stray --flags', as_root=True)
self.assertTrue(self.device.PathExists(self.cmdline_path_legacy))
changer = flag_changer.FlagChanger(self.device, _CMDLINE_FILE)
# Legacy command line file is removed, ensuring Chrome picks up the
# right file.
self.assertFalse(self.device.PathExists(self.cmdline_path_legacy))
# Write some new files, and check they are set.
new_flags = ['--my', '--new', '--flags=with special value']
self.assertItemsEqual(
changer.ReplaceFlags(new_flags),
new_flags)
# Restore and go back to the old flags.
self.assertItemsEqual(
changer.Restore(),
['--some', '--old', '--flags'])
def testFlagChanger_removeFlags(self):
self.device.RemovePath(self.cmdline_path, force=True)
self.assertFalse(self.device.PathExists(self.cmdline_path))
with flag_changer.CustomCommandLineFlags(
self.device, _CMDLINE_FILE, ['--some', '--flags']):
self.assertTrue(self.device.PathExists(self.cmdline_path))
self.assertFalse(self.device.PathExists(self.cmdline_path))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python
# Copyright 2017 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 posixpath
import unittest
from devil.android import flag_changer
_CMDLINE_FILE = 'chrome-command-line'
class _FakeDevice(object):
def __init__(self):
self.build_type = 'user'
self.has_root = True
self.file_system = {}
def HasRoot(self):
return self.has_root
def PathExists(self, filepath):
return filepath in self.file_system
def RemovePath(self, path, **_kwargs):
self.file_system.pop(path)
def WriteFile(self, path, contents, **_kwargs):
self.file_system[path] = contents
def ReadFile(self, path, **_kwargs):
return self.file_system[path]
class FlagChangerTest(unittest.TestCase):
def setUp(self):
self.device = _FakeDevice()
# pylint: disable=protected-access
self.cmdline_path = posixpath.join(flag_changer._CMDLINE_DIR, _CMDLINE_FILE)
self.cmdline_path_legacy = posixpath.join(
flag_changer._CMDLINE_DIR_LEGACY, _CMDLINE_FILE)
def testFlagChanger_removeAlternateCmdLine(self):
self.device.WriteFile(self.cmdline_path_legacy, 'chrome --old --stuff')
self.assertTrue(self.device.PathExists(self.cmdline_path_legacy))
changer = flag_changer.FlagChanger(self.device, 'chrome-command-line')
self.assertEquals(
changer._cmdline_path, # pylint: disable=protected-access
self.cmdline_path)
self.assertFalse(self.device.PathExists(self.cmdline_path_legacy))
def testFlagChanger_removeAlternateCmdLineLegacyPath(self):
self.device.WriteFile(self.cmdline_path, 'chrome --old --stuff')
self.assertTrue(self.device.PathExists(self.cmdline_path))
changer = flag_changer.FlagChanger(self.device, 'chrome-command-line',
use_legacy_path=True)
self.assertEquals(
changer._cmdline_path, # pylint: disable=protected-access
self.cmdline_path_legacy)
self.assertFalse(self.device.PathExists(self.cmdline_path))
def testFlagChanger_mustBeFileName(self):
with self.assertRaises(ValueError):
flag_changer.FlagChanger(self.device, '/data/local/chrome-command-line')
class ParseSerializeFlagsTest(unittest.TestCase):
def _testQuoteFlag(self, flag, expected_quoted_flag):
# Start with an unquoted flag, check that it's quoted as expected.
# pylint: disable=protected-access
quoted_flag = flag_changer._QuoteFlag(flag)
self.assertEqual(quoted_flag, expected_quoted_flag)
# Check that it survives a round-trip.
parsed_flags = flag_changer._ParseFlags('_ %s' % quoted_flag)
self.assertEqual(len(parsed_flags), 1)
self.assertEqual(flag, parsed_flags[0])
def testQuoteFlag_simple(self):
self._testQuoteFlag('--simple-flag', '--simple-flag')
def testQuoteFlag_withSimpleValue(self):
self._testQuoteFlag('--key=value', '--key=value')
def testQuoteFlag_withQuotedValue1(self):
self._testQuoteFlag('--key=valueA valueB', '--key="valueA valueB"')
def testQuoteFlag_withQuotedValue2(self):
self._testQuoteFlag(
'--key=this "should" work', r'--key="this \"should\" work"')
def testQuoteFlag_withQuotedValue3(self):
self._testQuoteFlag(
"--key=this is 'fine' too", '''--key="this is 'fine' too"''')
def testQuoteFlag_withQuotedValue4(self):
self._testQuoteFlag(
"--key='I really want to keep these quotes'",
'''--key="'I really want to keep these quotes'"''')
def testQuoteFlag_withQuotedValue5(self):
self._testQuoteFlag(
"--this is a strange=flag", '"--this is a strange=flag"')
def testQuoteFlag_withEmptyValue(self):
self._testQuoteFlag('--some-flag=', '--some-flag=')
def _testParseCmdLine(self, command_line, expected_flags):
# Start with a command line, check that flags are parsed as expected.
# pylint: disable=protected-access
flags = flag_changer._ParseFlags(command_line)
self.assertItemsEqual(flags, expected_flags)
# Check that flags survive a round-trip.
# Note: Although new_command_line and command_line may not match, they
# should describe the same set of flags.
new_command_line = flag_changer._SerializeFlags(flags)
new_flags = flag_changer._ParseFlags(new_command_line)
self.assertItemsEqual(new_flags, expected_flags)
def testParseCmdLine_simple(self):
self._testParseCmdLine(
'chrome --foo --bar="a b" --baz=true --fine="ok"',
['--foo', '--bar=a b', '--baz=true', '--fine=ok'])
def testParseCmdLine_withFancyQuotes(self):
self._testParseCmdLine(
r'''_ --foo="this 'is' ok"
--bar='this \'is\' too'
--baz="this \'is\' tricky"
''',
["--foo=this 'is' ok",
"--bar=this 'is' too",
r"--baz=this \'is\' tricky"])
def testParseCmdLine_withUnterminatedQuote(self):
self._testParseCmdLine(
'_ --foo --bar="I forgot something',
['--foo', '--bar=I forgot something'])
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,476 @@
# 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.
# pylint: disable=W0212
import fcntl
import inspect
import logging
import os
import psutil
from devil import base_error
from devil import devil_env
from devil.android import device_errors
from devil.android.constants import file_system
from devil.android.sdk import adb_wrapper
from devil.android.valgrind_tools import base_tool
from devil.utils import cmd_helper
logger = logging.getLogger(__name__)
# If passed as the device port, this will tell the forwarder to allocate
# a dynamic port on the device. The actual port can then be retrieved with
# Forwarder.DevicePortForHostPort.
DYNAMIC_DEVICE_PORT = 0
def _GetProcessStartTime(pid):
p = psutil.Process(pid)
if inspect.ismethod(p.create_time):
return p.create_time()
else: # Process.create_time is a property in old versions of psutil.
return p.create_time
def _LogMapFailureDiagnostics(device):
# The host forwarder daemon logs to /tmp/host_forwarder_log, so print the end
# of that.
try:
with open('/tmp/host_forwarder_log') as host_forwarder_log:
logger.info('Last 50 lines of the host forwarder daemon log:')
for line in host_forwarder_log.read().splitlines()[-50:]:
logger.info(' %s', line)
except Exception: # pylint: disable=broad-except
# Grabbing the host forwarder log is best-effort. Ignore all errors.
logger.warning('Failed to get the contents of host_forwarder_log.')
# The device forwarder daemon logs to the logcat, so print the end of that.
try:
logger.info('Last 50 lines of logcat:')
for logcat_line in device.adb.Logcat(dump=True)[-50:]:
logger.info(' %s', logcat_line)
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
# Grabbing the device forwarder log is also best-effort. Ignore all errors.
logger.warning('Failed to get the contents of the logcat.')
# Log alive device forwarders.
try:
ps_out = device.RunShellCommand(['ps'], check_return=True)
logger.info('Currently running device_forwarders:')
for line in ps_out:
if 'device_forwarder' in line:
logger.info(' %s', line)
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.warning('Failed to list currently running device_forwarder '
'instances.')
class _FileLock(object):
"""With statement-aware implementation of a file lock.
File locks are needed for cross-process synchronization when the
multiprocessing Python module is used.
"""
def __init__(self, path):
self._fd = -1
self._path = path
def __enter__(self):
self._fd = os.open(self._path, os.O_RDONLY | os.O_CREAT)
if self._fd < 0:
raise Exception('Could not open file %s for reading' % self._path)
fcntl.flock(self._fd, fcntl.LOCK_EX)
def __exit__(self, _exception_type, _exception_value, traceback):
fcntl.flock(self._fd, fcntl.LOCK_UN)
os.close(self._fd)
class HostForwarderError(base_error.BaseError):
"""Exception for failures involving host_forwarder."""
def __init__(self, message):
super(HostForwarderError, self).__init__(message)
class Forwarder(object):
"""Thread-safe class to manage port forwards from the device to the host."""
_DEVICE_FORWARDER_FOLDER = (file_system.TEST_EXECUTABLE_DIR +
'/forwarder/')
_DEVICE_FORWARDER_PATH = (file_system.TEST_EXECUTABLE_DIR +
'/forwarder/device_forwarder')
_LOCK_PATH = '/tmp/chrome.forwarder.lock'
# Defined in host_forwarder_main.cc
_HOST_FORWARDER_LOG = '/tmp/host_forwarder_log'
_TIMEOUT = 60 # seconds
_instance = None
@staticmethod
def Map(port_pairs, device, tool=None):
"""Runs the forwarder.
Args:
port_pairs: A list of tuples (device_port, host_port) to forward. Note
that you can specify 0 as a device_port, in which case a
port will by dynamically assigned on the device. You can
get the number of the assigned port using the
DevicePortForHostPort method.
device: A DeviceUtils instance.
tool: Tool class to use to get wrapper, if necessary, for executing the
forwarder (see valgrind_tools.py).
Raises:
Exception on failure to forward the port.
"""
if not tool:
tool = base_tool.BaseTool()
with _FileLock(Forwarder._LOCK_PATH):
instance = Forwarder._GetInstanceLocked(tool)
instance._InitDeviceLocked(device, tool)
device_serial = str(device)
map_arg_lists = [
['--adb=' + adb_wrapper.AdbWrapper.GetAdbPath(),
'--serial-id=' + device_serial,
'--map', str(device_port), str(host_port)]
for device_port, host_port in port_pairs]
logger.info('Forwarding using commands: %s', map_arg_lists)
for map_arg_list in map_arg_lists:
try:
map_cmd = [instance._host_forwarder_path] + map_arg_list
(exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
map_cmd, Forwarder._TIMEOUT)
except cmd_helper.TimeoutError as e:
raise HostForwarderError(
'`%s` timed out:\n%s' % (' '.join(map_cmd), e.output))
except OSError as e:
if e.errno == 2:
raise HostForwarderError(
'Unable to start host forwarder. '
'Make sure you have built host_forwarder.')
else: raise
if exit_code != 0:
try:
instance._KillDeviceLocked(device, tool)
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
# We don't want the failure to kill the device forwarder to
# supersede the original failure to map.
logger.warning(
'Failed to kill the device forwarder after map failure: %s',
str(e))
_LogMapFailureDiagnostics(device)
formatted_output = ('\n'.join(output) if isinstance(output, list)
else output)
raise HostForwarderError(
'`%s` exited with %d:\n%s' % (
' '.join(map_cmd),
exit_code,
formatted_output))
tokens = output.split(':')
if len(tokens) != 2:
raise HostForwarderError(
'Unexpected host forwarder output "%s", '
'expected "device_port:host_port"' % output)
device_port = int(tokens[0])
host_port = int(tokens[1])
serial_with_port = (device_serial, device_port)
instance._device_to_host_port_map[serial_with_port] = host_port
instance._host_to_device_port_map[host_port] = serial_with_port
logger.info('Forwarding device port: %d to host port: %d.',
device_port, host_port)
@staticmethod
def UnmapDevicePort(device_port, device):
"""Unmaps a previously forwarded device port.
Args:
device: A DeviceUtils instance.
device_port: A previously forwarded port (through Map()).
"""
with _FileLock(Forwarder._LOCK_PATH):
Forwarder._UnmapDevicePortLocked(device_port, device)
@staticmethod
def UnmapAllDevicePorts(device):
"""Unmaps all the previously forwarded ports for the provided device.
Args:
device: A DeviceUtils instance.
port_pairs: A list of tuples (device_port, host_port) to unmap.
"""
with _FileLock(Forwarder._LOCK_PATH):
instance = Forwarder._GetInstanceLocked(None)
unmap_all_cmd = [
instance._host_forwarder_path,
'--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
'--serial-id=%s' % device.serial,
'--unmap-all'
]
try:
exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
unmap_all_cmd, Forwarder._TIMEOUT)
except cmd_helper.TimeoutError as e:
raise HostForwarderError(
'`%s` timed out:\n%s' % (' '.join(unmap_all_cmd), e.output))
if exit_code != 0:
error_msg = [
'`%s` exited with %d' % (' '.join(unmap_all_cmd), exit_code)]
if isinstance(output, list):
error_msg += output
else:
error_msg += [output]
raise HostForwarderError('\n'.join(error_msg))
# Clean out any entries from the device & host map.
device_map = instance._device_to_host_port_map
host_map = instance._host_to_device_port_map
for device_serial_and_port, host_port in device_map.items():
device_serial = device_serial_and_port[0]
if device_serial == device.serial:
del device_map[device_serial_and_port]
del host_map[host_port]
# Kill the device forwarder.
tool = base_tool.BaseTool()
instance._KillDeviceLocked(device, tool)
@staticmethod
def DevicePortForHostPort(host_port):
"""Returns the device port that corresponds to a given host port."""
with _FileLock(Forwarder._LOCK_PATH):
serial_and_port = Forwarder._GetInstanceLocked(
None)._host_to_device_port_map.get(host_port)
return serial_and_port[1] if serial_and_port else None
@staticmethod
def RemoveHostLog():
if os.path.exists(Forwarder._HOST_FORWARDER_LOG):
os.unlink(Forwarder._HOST_FORWARDER_LOG)
@staticmethod
def GetHostLog():
if not os.path.exists(Forwarder._HOST_FORWARDER_LOG):
return ''
with file(Forwarder._HOST_FORWARDER_LOG, 'r') as f:
return f.read()
@staticmethod
def _GetInstanceLocked(tool):
"""Returns the singleton instance.
Note that the global lock must be acquired before calling this method.
Args:
tool: Tool class to use to get wrapper, if necessary, for executing the
forwarder (see valgrind_tools.py).
"""
if not Forwarder._instance:
Forwarder._instance = Forwarder(tool)
return Forwarder._instance
def __init__(self, tool):
"""Constructs a new instance of Forwarder.
Note that Forwarder is a singleton therefore this constructor should be
called only once.
Args:
tool: Tool class to use to get wrapper, if necessary, for executing the
forwarder (see valgrind_tools.py).
"""
assert not Forwarder._instance
self._tool = tool
self._initialized_devices = set()
self._device_to_host_port_map = dict()
self._host_to_device_port_map = dict()
self._host_forwarder_path = devil_env.config.FetchPath('forwarder_host')
assert os.path.exists(self._host_forwarder_path), 'Please build forwarder2'
self._InitHostLocked()
@staticmethod
def _UnmapDevicePortLocked(device_port, device):
"""Internal method used by UnmapDevicePort().
Note that the global lock must be acquired before calling this method.
"""
instance = Forwarder._GetInstanceLocked(None)
serial = str(device)
serial_with_port = (serial, device_port)
if serial_with_port not in instance._device_to_host_port_map:
logger.error('Trying to unmap non-forwarded port %d', device_port)
return
host_port = instance._device_to_host_port_map[serial_with_port]
del instance._device_to_host_port_map[serial_with_port]
del instance._host_to_device_port_map[host_port]
unmap_cmd = [
instance._host_forwarder_path,
'--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
'--serial-id=%s' % serial,
'--unmap', str(device_port)
]
try:
(exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
unmap_cmd, Forwarder._TIMEOUT)
except cmd_helper.TimeoutError as e:
raise HostForwarderError(
'`%s` timed out:\n%s' % (' '.join(unmap_cmd), e.output))
if exit_code != 0:
logger.error(
'`%s` exited with %d:\n%s',
' '.join(unmap_cmd),
exit_code,
'\n'.join(output) if isinstance(output, list) else output)
@staticmethod
def _GetPidForLock():
"""Returns the PID used for host_forwarder initialization.
The PID of the "sharder" is used to handle multiprocessing. The "sharder"
is the initial process that forks that is the parent process.
"""
return os.getpgrp()
def _InitHostLocked(self):
"""Initializes the host forwarder daemon.
Note that the global lock must be acquired before calling this method. This
method kills any existing host_forwarder process that could be stale.
"""
# See if the host_forwarder daemon was already initialized by a concurrent
# process or thread (in case multi-process sharding is not used).
# TODO(crbug.com/762005): Consider using a different implemention; relying
# on matching the string represantion of the process start time seems
# fragile.
pid_for_lock = Forwarder._GetPidForLock()
fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
with os.fdopen(fd, 'r+') as pid_file:
pid_with_start_time = pid_file.readline()
if pid_with_start_time:
(pid, process_start_time) = pid_with_start_time.split(':')
if pid == str(pid_for_lock):
if process_start_time == str(_GetProcessStartTime(pid_for_lock)):
return
self._KillHostLocked()
pid_file.seek(0)
pid_file.write(
'%s:%s' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock))))
pid_file.truncate()
def _InitDeviceLocked(self, device, tool):
"""Initializes the device_forwarder daemon for a specific device (once).
Note that the global lock must be acquired before calling this method. This
method kills any existing device_forwarder daemon on the device that could
be stale, pushes the latest version of the daemon (to the device) and starts
it.
Args:
device: A DeviceUtils instance.
tool: Tool class to use to get wrapper, if necessary, for executing the
forwarder (see valgrind_tools.py).
"""
device_serial = str(device)
if device_serial in self._initialized_devices:
return
try:
self._KillDeviceLocked(device, tool)
except device_errors.CommandFailedError:
logger.warning('Failed to kill device forwarder. Rebooting.')
device.Reboot()
forwarder_device_path_on_host = devil_env.config.FetchPath(
'forwarder_device', device=device)
forwarder_device_path_on_device = (
Forwarder._DEVICE_FORWARDER_FOLDER
if os.path.isdir(forwarder_device_path_on_host)
else Forwarder._DEVICE_FORWARDER_PATH)
device.PushChangedFiles([(
forwarder_device_path_on_host,
forwarder_device_path_on_device)])
cmd = [Forwarder._DEVICE_FORWARDER_PATH]
wrapper = tool.GetUtilWrapper()
if wrapper:
cmd.insert(0, wrapper)
device.RunShellCommand(
cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER},
check_return=True)
self._initialized_devices.add(device_serial)
@staticmethod
def KillHost():
"""Kills the forwarder process running on the host."""
with _FileLock(Forwarder._LOCK_PATH):
Forwarder._GetInstanceLocked(None)._KillHostLocked()
def _KillHostLocked(self):
"""Kills the forwarder process running on the host.
Note that the global lock must be acquired before calling this method.
"""
logger.info('Killing host_forwarder.')
try:
kill_cmd = [self._host_forwarder_path, '--kill-server']
(exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
kill_cmd, Forwarder._TIMEOUT)
if exit_code != 0:
logger.warning('Forwarder unable to shut down:\n%s', output)
kill_cmd = ['pkill', '-9', 'host_forwarder']
(exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
kill_cmd, Forwarder._TIMEOUT)
if exit_code != 0:
raise HostForwarderError(
'%s exited with %d:\n%s' % (
self._host_forwarder_path,
exit_code,
'\n'.join(output) if isinstance(output, list) else output))
except cmd_helper.TimeoutError as e:
raise HostForwarderError(
'`%s` timed out:\n%s' % (' '.join(kill_cmd), e.output))
@staticmethod
def KillDevice(device, tool=None):
"""Kills the forwarder process running on the device.
Args:
device: Instance of DeviceUtils for talking to the device.
tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
forwarder (see valgrind_tools.py).
"""
with _FileLock(Forwarder._LOCK_PATH):
Forwarder._GetInstanceLocked(None)._KillDeviceLocked(
device, tool or base_tool.BaseTool())
def _KillDeviceLocked(self, device, tool):
"""Kills the forwarder process running on the device.
Note that the global lock must be acquired before calling this method.
Args:
device: Instance of DeviceUtils for talking to the device.
tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
forwarder (see valgrind_tools.py).
"""
logger.info('Killing device_forwarder.')
self._initialized_devices.discard(device.serial)
if not device.FileExists(Forwarder._DEVICE_FORWARDER_PATH):
return
cmd = [Forwarder._DEVICE_FORWARDER_PATH, '--kill-server']
wrapper = tool.GetUtilWrapper()
if wrapper:
cmd.insert(0, wrapper)
device.RunShellCommand(
cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER},
check_return=True)

View File

@@ -0,0 +1,57 @@
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import posixpath
from devil import devil_env
from devil.android import device_errors
from devil.android.constants import file_system
BIN_DIR = '%s/bin' % file_system.TEST_EXECUTABLE_DIR
_FRAMEWORK_DIR = '%s/framework' % file_system.TEST_EXECUTABLE_DIR
_COMMANDS = {
'unzip': 'org.chromium.android.commands.unzip.Unzip',
}
_SHELL_COMMAND_FORMAT = (
"""#!/system/bin/sh
base=%s
export CLASSPATH=$base/framework/chromium_commands.jar
exec app_process $base/bin %s $@
""")
def Installed(device):
paths = [posixpath.join(BIN_DIR, c) for c in _COMMANDS]
paths.append(posixpath.join(_FRAMEWORK_DIR, 'chromium_commands.jar'))
return device.PathExists(paths)
def InstallCommands(device):
if device.IsUserBuild():
raise device_errors.CommandFailedError(
'chromium_commands currently requires a userdebug build.',
device_serial=device.adb.GetDeviceSerial())
chromium_commands_jar_path = devil_env.config.FetchPath('chromium_commands')
if not os.path.exists(chromium_commands_jar_path):
raise device_errors.CommandFailedError(
'%s not found. Please build chromium_commands.'
% chromium_commands_jar_path)
device.RunShellCommand(
['mkdir', '-p', BIN_DIR, _FRAMEWORK_DIR], check_return=True)
for command, main_class in _COMMANDS.iteritems():
shell_command = _SHELL_COMMAND_FORMAT % (
file_system.TEST_EXECUTABLE_DIR, main_class)
shell_file = '%s/%s' % (BIN_DIR, command)
device.WriteFile(shell_file, shell_command)
device.RunShellCommand(
['chmod', '755', shell_file], check_return=True)
device.adb.Push(
chromium_commands_jar_path,
'%s/chromium_commands.jar' % _FRAMEWORK_DIR)

View File

@@ -0,0 +1,273 @@
# 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 errno
import logging
import os
import re
import shutil
import tempfile
import threading
import time
from devil.android import decorators
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
from devil.utils import reraiser_thread
logger = logging.getLogger(__name__)
class LogcatMonitor(object):
_RECORD_ITER_TIMEOUT = 0.2
_RECORD_THREAD_JOIN_WAIT = 5.0
_WAIT_TIME = 0.2
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, output_file=None,
transform_func=None, check_error=True):
"""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.
output_file: File path to save recorded logcat.
transform_func: An optional unary callable that takes and returns
a list of lines, possibly transforming them in the process.
check_error: Check for and raise an exception on nonzero exit codes
from the underlying logcat command.
"""
if isinstance(adb, adb_wrapper.AdbWrapper):
self._adb = adb
else:
raise ValueError('Unsupported type passed for argument "device"')
self._check_error = check_error
self._clear = clear
self._filter_specs = filter_specs
self._output_file = output_file
self._record_file = None
self._record_file_lock = threading.Lock()
self._record_thread = None
self._stop_recording_event = threading.Event()
self._transform_func = transform_func
@property
def output_file(self):
return self._output_file
@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.
LogcatMonitorCommandError when calling |WaitFor| while not recording
logcat.
"""
if self._record_thread is None:
raise LogcatMonitorCommandError(
'Must be recording logcat when calling |WaitFor|',
device_serial=str(self._adb))
if isinstance(success_regex, basestring):
success_regex = re.compile(success_regex)
if isinstance(failure_regex, basestring):
failure_regex = re.compile(failure_regex)
logger.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.
with open(self._record_file.name, 'r') as f:
while True:
line = f.readline()
if line:
m = success_regex.search(line)
if m:
return m
if failure_regex and failure_regex.search(line):
return None
else:
time.sleep(self._WAIT_TIME)
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.
Raises:
LogcatMonitorCommandError when calling |FindAll| before recording logcat.
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 self._record_file is None:
raise LogcatMonitorCommandError(
'Must have recorded or be recording a logcat to call |FindAll|',
device_serial=str(self._adb))
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:]+'
# pylint: disable=protected-access
threadtime_re = re.compile(
type(self).THREADTIME_RE_FORMAT % (
proc_id, thread_id, log_level, component, message_regex))
with open(self._record_file.name, 'r') as f:
for line in f:
m = re.match(threadtime_re, line)
if m:
yield m
def _StartRecording(self):
"""Starts recording logcat to file.
Function spawns a thread that records logcat to file and will not die
until |StopRecording| is called.
"""
def record_to_file():
# Write the log with line buffering so the consumer sees each individual
# line.
for data in self._adb.Logcat(
filter_specs=self._filter_specs,
logcat_format='threadtime',
iter_timeout=self._RECORD_ITER_TIMEOUT,
check_error=self._check_error):
if self._stop_recording_event.isSet():
return
if data is None:
# Logcat can yield None if the iter_timeout is hit.
continue
with self._record_file_lock:
if self._record_file and not self._record_file.closed:
if self._transform_func:
data = '\n'.join(self._transform_func([data]))
self._record_file.write(data + '\n')
self._stop_recording_event.clear()
if not self._record_thread:
self._record_thread = reraiser_thread.ReraiserThread(record_to_file)
self._record_thread.start()
def _StopRecording(self):
"""Finish recording logcat."""
if self._record_thread:
self._stop_recording_event.set()
self._record_thread.join(timeout=self._RECORD_THREAD_JOIN_WAIT)
self._record_thread.ReraiseIfException()
self._record_thread = None
def Start(self):
"""Starts the logcat monitor.
Clears the logcat if |clear| was set in |__init__|.
"""
if self._clear:
self._adb.Logcat(clear=True)
if not self._record_file:
self._record_file = tempfile.NamedTemporaryFile(mode='a', bufsize=1)
self._StartRecording()
def Stop(self):
"""Stops the logcat monitor.
Stops recording the logcat. Copies currently recorded logcat to
|self._output_file|.
"""
self._StopRecording()
with self._record_file_lock:
if self._record_file and self._output_file:
try:
os.makedirs(os.path.dirname(self._output_file))
except OSError as e:
if e.errno != errno.EEXIST:
raise
shutil.copy(self._record_file.name, self._output_file)
def Close(self):
"""Closes logcat recording file.
Should be called when finished using the logcat monitor.
"""
with self._record_file_lock:
if self._record_file:
self._record_file.close()
self._record_file = None
def close(self):
"""An alias for Close.
Allows LogcatMonitors to be used with contextlib.closing.
"""
self.Close()
def __enter__(self):
"""Starts the logcat monitor."""
self.Start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Stops the logcat monitor."""
self.Stop()
def __del__(self):
"""Closes logcat recording file in case |Close| was never called."""
with self._record_file_lock:
if self._record_file:
logger.warning(
'Need to call |Close| on the logcat monitor when done!')
self._record_file.close()
@property
def adb(self):
return self._adb
class LogcatMonitorCommandError(device_errors.CommandFailedError):
"""Exception for errors with logcat monitor commands."""
pass

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python
# 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=protected-access
import itertools
import threading
import unittest
from devil import devil_env
from devil.android import logcat_monitor
from devil.android.sdk import adb_wrapper
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
def _CreateTestLog(raw_logcat=None):
test_adb = adb_wrapper.AdbWrapper('0123456789abcdef')
test_adb.Logcat = mock.Mock(return_value=(l for l in raw_logcat))
test_log = logcat_monitor.LogcatMonitor(test_adb, clear=False)
return test_log
class LogcatMonitorTest(unittest.TestCase):
_TEST_THREADTIME_LOGCAT_DATA = [
'01-01 01:02:03.456 7890 0987 V LogcatMonitorTest: '
'verbose logcat monitor test message 1',
'01-01 01:02:03.457 8901 1098 D LogcatMonitorTest: '
'debug logcat monitor test message 2',
'01-01 01:02:03.458 9012 2109 I LogcatMonitorTest: '
'info logcat monitor test message 3',
'01-01 01:02:03.459 0123 3210 W LogcatMonitorTest: '
'warning logcat monitor test message 4',
'01-01 01:02:03.460 1234 4321 E LogcatMonitorTest: '
'error logcat monitor test message 5',
'01-01 01:02:03.461 2345 5432 F LogcatMonitorTest: '
'fatal logcat monitor test message 6',
'01-01 01:02:03.462 3456 6543 D LogcatMonitorTest: '
'last line'
]
def assertIterEqual(self, expected_iter, actual_iter):
for expected, actual in itertools.izip_longest(expected_iter, actual_iter):
self.assertIsNotNone(
expected,
msg='actual has unexpected elements starting with %s' % str(actual))
self.assertIsNotNone(
actual,
msg='actual is missing elements starting with %s' % str(expected))
self.assertEqual(actual.group('proc_id'), expected[0])
self.assertEqual(actual.group('thread_id'), expected[1])
self.assertEqual(actual.group('log_level'), expected[2])
self.assertEqual(actual.group('component'), expected[3])
self.assertEqual(actual.group('message'), expected[4])
with self.assertRaises(StopIteration):
next(actual_iter)
with self.assertRaises(StopIteration):
next(expected_iter)
@mock.patch('time.sleep', mock.Mock())
def testWaitFor_success(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
actual_match = test_log.WaitFor(r'.*(fatal|error) logcat monitor.*', None)
self.assertTrue(actual_match)
self.assertEqual(
'01-01 01:02:03.460 1234 4321 E LogcatMonitorTest: '
'error logcat monitor test message 5',
actual_match.group(0))
self.assertEqual('error', actual_match.group(1))
test_log.Stop()
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testWaitFor_failure(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
actual_match = test_log.WaitFor(
r'.*My Success Regex.*', r'.*(fatal|error) logcat monitor.*')
self.assertIsNone(actual_match)
test_log.Stop()
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testWaitFor_buffering(self):
# Simulate an adb log stream which does not complete until the test tells it
# to. This checks that the log matcher can receive individual lines from the
# log reader thread even if adb is not producing enough output to fill an
# entire file io buffer.
finished_lock = threading.Lock()
finished_lock.acquire()
def LogGenerator():
for line in type(self)._TEST_THREADTIME_LOGCAT_DATA:
yield line
finished_lock.acquire()
test_adb = adb_wrapper.AdbWrapper('0123456789abcdef')
test_adb.Logcat = mock.Mock(return_value=LogGenerator())
test_log = logcat_monitor.LogcatMonitor(test_adb, clear=False)
test_log.Start()
actual_match = test_log.WaitFor(r'.*last line.*', None)
finished_lock.release()
self.assertTrue(actual_match)
test_log.Stop()
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testFindAll_defaults(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
test_log.WaitFor(r'.*last line.*', None)
test_log.Stop()
expected_results = [
('7890', '0987', 'V', 'LogcatMonitorTest',
'verbose logcat monitor test message 1'),
('8901', '1098', 'D', 'LogcatMonitorTest',
'debug logcat monitor test message 2'),
('9012', '2109', 'I', 'LogcatMonitorTest',
'info logcat monitor test message 3'),
('0123', '3210', 'W', 'LogcatMonitorTest',
'warning logcat monitor test message 4'),
('1234', '4321', 'E', 'LogcatMonitorTest',
'error logcat monitor test message 5'),
('2345', '5432', 'F', 'LogcatMonitorTest',
'fatal logcat monitor test message 6')]
actual_results = test_log.FindAll(r'\S* logcat monitor test message \d')
self.assertIterEqual(iter(expected_results), actual_results)
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testFindAll_defaults_miss(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
test_log.WaitFor(r'.*last line.*', None)
test_log.Stop()
expected_results = []
actual_results = test_log.FindAll(r'\S* nothing should match this \d')
self.assertIterEqual(iter(expected_results), actual_results)
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testFindAll_filterProcId(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
test_log.WaitFor(r'.*last line.*', None)
test_log.Stop()
actual_results = test_log.FindAll(
r'\S* logcat monitor test message \d', proc_id=1234)
expected_results = [
('1234', '4321', 'E', 'LogcatMonitorTest',
'error logcat monitor test message 5')]
self.assertIterEqual(iter(expected_results), actual_results)
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testFindAll_filterThreadId(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
test_log.WaitFor(r'.*last line.*', None)
test_log.Stop()
actual_results = test_log.FindAll(
r'\S* logcat monitor test message \d', thread_id=2109)
expected_results = [
('9012', '2109', 'I', 'LogcatMonitorTest',
'info logcat monitor test message 3')]
self.assertIterEqual(iter(expected_results), actual_results)
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testFindAll_filterLogLevel(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
test_log.WaitFor(r'.*last line.*', None)
test_log.Stop()
actual_results = test_log.FindAll(
r'\S* logcat monitor test message \d', log_level=r'[DW]')
expected_results = [
('8901', '1098', 'D', 'LogcatMonitorTest',
'debug logcat monitor test message 2'),
('0123', '3210', 'W', 'LogcatMonitorTest',
'warning logcat monitor test message 4')
]
self.assertIterEqual(iter(expected_results), actual_results)
test_log.Close()
@mock.patch('time.sleep', mock.Mock())
def testFindAll_filterComponent(self):
test_log = _CreateTestLog(
raw_logcat=type(self)._TEST_THREADTIME_LOGCAT_DATA)
test_log.Start()
test_log.WaitFor(r'.*last line.*', None)
test_log.Stop()
actual_results = test_log.FindAll(r'.*', component='LogcatMonitorTest')
expected_results = [
('7890', '0987', 'V', 'LogcatMonitorTest',
'verbose logcat monitor test message 1'),
('8901', '1098', 'D', 'LogcatMonitorTest',
'debug logcat monitor test message 2'),
('9012', '2109', 'I', 'LogcatMonitorTest',
'info logcat monitor test message 3'),
('0123', '3210', 'W', 'LogcatMonitorTest',
'warning logcat monitor test message 4'),
('1234', '4321', 'E', 'LogcatMonitorTest',
'error logcat monitor test message 5'),
('2345', '5432', 'F', 'LogcatMonitorTest',
'fatal logcat monitor test message 6'),
('3456', '6543', 'D', 'LogcatMonitorTest',
'last line')
]
self.assertIterEqual(iter(expected_results), actual_results)
test_log.Close()
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,122 @@
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import posixpath
import re
from devil import devil_env
from devil.android import device_errors
from devil.utils import cmd_helper
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 = devil_env.config.FetchPath('md5sum_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] + [os.path.realpath(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 = devil_env.config.FetchPath('md5sum_device', device=device)
if os.path.isdir(md5sum_dist_path):
md5sum_dist_bin_path = os.path.join(md5sum_dist_path, 'md5sum_bin')
else:
md5sum_dist_bin_path = md5sum_dist_path
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, shell=True, check_return=True, large_output=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)
if os.path.isdir(md5sum_dist_path):
device.adb.Push(md5sum_dist_path, MD5SUM_DEVICE_LIB_PATH)
else:
mkdir_cmd = 'a=%s;[[ -e $a ]] || mkdir $a' % MD5SUM_DEVICE_LIB_PATH
device.RunShellCommand(mkdir_cmd, shell=True, check_return=True)
device.adb.Push(md5sum_dist_bin_path, MD5SUM_DEVICE_BIN_PATH)
out = device.RunShellCommand(
md5sum_script, shell=True, check_return=True, large_output=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)

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env python
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import unittest
from devil import devil_env
from devil.android import device_errors
from devil.android import md5sum
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
TEST_OUT_DIR = os.path.join('test', 'out', 'directory')
HOST_MD5_EXECUTABLE = os.path.join(TEST_OUT_DIR, 'md5sum_bin_host')
MD5_DIST = os.path.join(TEST_OUT_DIR, 'md5sum_dist')
class Md5SumTest(unittest.TestCase):
def setUp(self):
mocked_attrs = {
'md5sum_host': HOST_MD5_EXECUTABLE,
'md5sum_device': MD5_DIST,
}
self._patchers = [
mock.patch('devil.devil_env._Environment.FetchPath',
mock.Mock(side_effect=lambda a, device=None: mocked_attrs[a])),
mock.patch('os.path.exists',
new=mock.Mock(return_value=True)),
]
for p in self._patchers:
p.start()
def tearDown(self):
for p in self._patchers:
p.stop()
def testCalculateHostMd5Sums_singlePath(self):
test_path = '/test/host/file.dat'
mock_get_cmd_output = mock.Mock(
return_value='0123456789abcdeffedcba9876543210 /test/host/file.dat')
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)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/test/host/file.dat'])
mock_get_cmd_output.assert_called_once_with(
[HOST_MD5_EXECUTABLE, '/test/host/file.dat'])
def testCalculateHostMd5Sums_list(self):
test_paths = ['/test/host/file0.dat', '/test/host/file1.dat']
mock_get_cmd_output = mock.Mock(
return_value='0123456789abcdeffedcba9876543210 /test/host/file0.dat\n'
'123456789abcdef00fedcba987654321 /test/host/file1.dat\n')
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)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/test/host/file0.dat'])
self.assertTrue('/test/host/file1.dat' in out)
self.assertEquals('123456789abcdef00fedcba987654321',
out['/test/host/file1.dat'])
mock_get_cmd_output.assert_called_once_with(
[HOST_MD5_EXECUTABLE, '/test/host/file0.dat',
'/test/host/file1.dat'])
def testCalculateHostMd5Sums_generator(self):
test_paths = ('/test/host/' + p for p in ['file0.dat', 'file1.dat'])
mock_get_cmd_output = mock.Mock(
return_value='0123456789abcdeffedcba9876543210 /test/host/file0.dat\n'
'123456789abcdef00fedcba987654321 /test/host/file1.dat\n')
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)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/test/host/file0.dat'])
self.assertTrue('/test/host/file1.dat' in out)
self.assertEquals('123456789abcdef00fedcba987654321',
out['/test/host/file1.dat'])
mock_get_cmd_output.assert_called_once_with(
[HOST_MD5_EXECUTABLE, '/test/host/file0.dat', '/test/host/file1.dat'])
def testCalculateDeviceMd5Sums_noPaths(self):
device = mock.NonCallableMock()
device.RunShellCommand = mock.Mock(side_effect=Exception())
out = md5sum.CalculateDeviceMd5Sums([], device)
self.assertEquals(0, len(out))
def testCalculateDeviceMd5Sums_singlePath(self):
test_path = '/storage/emulated/legacy/test/file.dat'
device = mock.NonCallableMock()
device_md5sum_output = [
'0123456789abcdeffedcba9876543210 '
'/storage/emulated/legacy/test/file.dat',
]
device.RunShellCommand = mock.Mock(return_value=device_md5sum_output)
with mock.patch('os.path.getsize', return_value=1337):
out = md5sum.CalculateDeviceMd5Sums(test_path, device)
self.assertEquals(1, len(out))
self.assertTrue('/storage/emulated/legacy/test/file.dat' in out)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/storage/emulated/legacy/test/file.dat'])
self.assertEquals(1, len(device.RunShellCommand.call_args_list))
def testCalculateDeviceMd5Sums_list(self):
test_path = ['/storage/emulated/legacy/test/file0.dat',
'/storage/emulated/legacy/test/file1.dat']
device = mock.NonCallableMock()
device_md5sum_output = [
'0123456789abcdeffedcba9876543210 '
'/storage/emulated/legacy/test/file0.dat',
'123456789abcdef00fedcba987654321 '
'/storage/emulated/legacy/test/file1.dat',
]
device.RunShellCommand = mock.Mock(return_value=device_md5sum_output)
with mock.patch('os.path.getsize', return_value=1337):
out = md5sum.CalculateDeviceMd5Sums(test_path, device)
self.assertEquals(2, len(out))
self.assertTrue('/storage/emulated/legacy/test/file0.dat' in out)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/storage/emulated/legacy/test/file0.dat'])
self.assertTrue('/storage/emulated/legacy/test/file1.dat' in out)
self.assertEquals('123456789abcdef00fedcba987654321',
out['/storage/emulated/legacy/test/file1.dat'])
self.assertEquals(1, len(device.RunShellCommand.call_args_list))
def testCalculateDeviceMd5Sums_generator(self):
test_path = ('/storage/emulated/legacy/test/file%d.dat' % n
for n in xrange(0, 2))
device = mock.NonCallableMock()
device_md5sum_output = [
'0123456789abcdeffedcba9876543210 '
'/storage/emulated/legacy/test/file0.dat',
'123456789abcdef00fedcba987654321 '
'/storage/emulated/legacy/test/file1.dat',
]
device.RunShellCommand = mock.Mock(return_value=device_md5sum_output)
with mock.patch('os.path.getsize', return_value=1337):
out = md5sum.CalculateDeviceMd5Sums(test_path, device)
self.assertEquals(2, len(out))
self.assertTrue('/storage/emulated/legacy/test/file0.dat' in out)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/storage/emulated/legacy/test/file0.dat'])
self.assertTrue('/storage/emulated/legacy/test/file1.dat' in out)
self.assertEquals('123456789abcdef00fedcba987654321',
out['/storage/emulated/legacy/test/file1.dat'])
self.assertEquals(1, len(device.RunShellCommand.call_args_list))
def testCalculateDeviceMd5Sums_singlePath_linkerWarning(self):
# See crbug/479966
test_path = '/storage/emulated/legacy/test/file.dat'
device = mock.NonCallableMock()
device_md5sum_output = [
'WARNING: linker: /data/local/tmp/md5sum/md5sum_bin: '
'unused DT entry: type 0x1d arg 0x15db',
'THIS_IS_NOT_A_VALID_CHECKSUM_ZZZ some random text',
'0123456789abcdeffedcba9876543210 '
'/storage/emulated/legacy/test/file.dat',
]
device.RunShellCommand = mock.Mock(return_value=device_md5sum_output)
with mock.patch('os.path.getsize', return_value=1337):
out = md5sum.CalculateDeviceMd5Sums(test_path, device)
self.assertEquals(1, len(out))
self.assertTrue('/storage/emulated/legacy/test/file.dat' in out)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/storage/emulated/legacy/test/file.dat'])
self.assertEquals(1, len(device.RunShellCommand.call_args_list))
def testCalculateDeviceMd5Sums_list_fileMissing(self):
test_path = ['/storage/emulated/legacy/test/file0.dat',
'/storage/emulated/legacy/test/file1.dat']
device = mock.NonCallableMock()
device_md5sum_output = [
'0123456789abcdeffedcba9876543210 '
'/storage/emulated/legacy/test/file0.dat',
'[0819/203513:ERROR:md5sum.cc(25)] Could not open file asdf',
'',
]
device.RunShellCommand = mock.Mock(return_value=device_md5sum_output)
with mock.patch('os.path.getsize', return_value=1337):
out = md5sum.CalculateDeviceMd5Sums(test_path, device)
self.assertEquals(1, len(out))
self.assertTrue('/storage/emulated/legacy/test/file0.dat' in out)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/storage/emulated/legacy/test/file0.dat'])
self.assertEquals(1, len(device.RunShellCommand.call_args_list))
def testCalculateDeviceMd5Sums_requiresBinary(self):
test_path = '/storage/emulated/legacy/test/file.dat'
device = mock.NonCallableMock()
device.adb = mock.NonCallableMock()
device.adb.Push = mock.Mock()
device_md5sum_output = [
'WARNING: linker: /data/local/tmp/md5sum/md5sum_bin: '
'unused DT entry: type 0x1d arg 0x15db',
'THIS_IS_NOT_A_VALID_CHECKSUM_ZZZ some random text',
'0123456789abcdeffedcba9876543210 '
'/storage/emulated/legacy/test/file.dat',
]
error = device_errors.AdbShellCommandFailedError('cmd', 'out', 2)
device.RunShellCommand = mock.Mock(
side_effect=(error, '', device_md5sum_output))
with mock.patch('os.path.isdir', return_value=True), (
mock.patch('os.path.getsize', return_value=1337)):
out = md5sum.CalculateDeviceMd5Sums(test_path, device)
self.assertEquals(1, len(out))
self.assertTrue('/storage/emulated/legacy/test/file.dat' in out)
self.assertEquals('0123456789abcdeffedcba9876543210',
out['/storage/emulated/legacy/test/file.dat'])
self.assertEquals(3, len(device.RunShellCommand.call_args_list))
device.adb.Push.assert_called_once_with(
'test/out/directory/md5sum_dist', '/data/local/tmp/md5sum')
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,6 @@
# Copyright 2019 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 NDK.

View File

@@ -0,0 +1,16 @@
# Copyright 2019 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.
"""Android NDK ABIs.
https://developer.android.com/ndk/guides/abis
These constants can be compared against the value of
devil.android.DeviceUtils.product_cpu_abi.
"""
ARM = 'armeabi-v7a'
ARM_64 = 'arm64-v8a'
X86 = 'x86'
X86_64 = 'x86_64'

View File

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

View File

@@ -0,0 +1,15 @@
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
class CacheControl(object):
_DROP_CACHES = '/proc/sys/vm/drop_caches'
def __init__(self, device):
self._device = device
def DropRamCaches(self):
"""Drops the filesystem ram caches for performance testing."""
self._device.RunShellCommand(['sync'], check_return=True, as_root=True)
self._device.WriteFile(CacheControl._DROP_CACHES, '3', as_root=True)

View File

@@ -0,0 +1,354 @@
# 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.
import atexit
import logging
import re
from devil.android import device_errors
logger = logging.getLogger(__name__)
_atexit_messages = set()
# Defines how to switch between the default performance configuration
# ('default_mode') and the mode for use when benchmarking ('high_perf_mode').
# For devices not in the list the defaults are to set up the scaling governor to
# 'performance' and reset it back to 'ondemand' when benchmarking is finished.
#
# The 'default_mode_governor' is mandatory to define, while
# 'high_perf_mode_governor' is not taken into account. The latter is because the
# governor 'performance' is currently used for all benchmarking on all devices.
#
# TODO(crbug.com/383566): Add definitions for all devices used in the perf
# waterfall.
_PERFORMANCE_MODE_DEFINITIONS = {
# Fire TV Edition - 4K
'AFTKMST12': {
'default_mode_governor': 'interactive',
},
# Pixel 3
'blueline': {
'high_perf_mode': {
'bring_cpu_cores_online': True,
# The SoC is Arm big.LITTLE. The cores 0..3 are LITTLE, the 4..7 are big.
'cpu_max_freq': {'0..3': 1228800, '4..7': 1536000},
'gpu_max_freq': 520000000,
},
'default_mode': {
'cpu_max_freq': {'0..3': 1766400, '4..7': 2649600},
'gpu_max_freq': 710000000,
},
'default_mode_governor': 'schedutil',
},
'GT-I9300': {
'default_mode_governor': 'pegasusq',
},
'Galaxy Nexus': {
'default_mode_governor': 'interactive',
},
# Pixel
'msm8996': {
'high_perf_mode': {
'bring_cpu_cores_online': True,
'cpu_max_freq': 1209600,
'gpu_max_freq': 315000000,
},
'default_mode': {
# The SoC is Arm big.LITTLE. The cores 0..1 are LITTLE, the 2..3 are big.
'cpu_max_freq': {'0..1': 1593600, '2..3': 2150400},
'gpu_max_freq': 624000000,
},
'default_mode_governor': 'sched',
},
'Nexus 7': {
'default_mode_governor': 'interactive',
},
'Nexus 10': {
'default_mode_governor': 'interactive',
},
'Nexus 4': {
'high_perf_mode': {
'bring_cpu_cores_online': True,
},
'default_mode_governor': 'ondemand',
},
'Nexus 5': {
# The list of possible GPU frequency values can be found in:
# /sys/class/kgsl/kgsl-3d0/gpu_available_frequencies.
# For CPU cores the possible frequency values are at:
# /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
'high_perf_mode': {
'bring_cpu_cores_online': True,
'cpu_max_freq': 1190400,
'gpu_max_freq': 200000000,
},
'default_mode': {
'cpu_max_freq': 2265600,
'gpu_max_freq': 450000000,
},
'default_mode_governor': 'ondemand',
},
'Nexus 5X': {
'high_perf_mode': {
'bring_cpu_cores_online': True,
'cpu_max_freq': 1248000,
'gpu_max_freq': 300000000,
},
'default_mode': {
'governor': 'ondemand',
# The SoC is ARM big.LITTLE. The cores 4..5 are big, the 0..3 are LITTLE.
'cpu_max_freq': {'0..3': 1440000, '4..5': 1824000},
'gpu_max_freq': 600000000,
},
'default_mode_governor': 'ondemand',
},
}
def _GetPerfModeDefinitions(product_model):
if product_model.startswith('AOSP on '):
product_model = product_model.replace('AOSP on ', '')
return _PERFORMANCE_MODE_DEFINITIONS.get(product_model)
def _NoisyWarning(message):
message += ' Results may be NOISY!!'
logger.warning(message)
# Add an additional warning at exit, such that it's clear that any results
# may be different/noisy (due to the lack of intended performance mode).
if message not in _atexit_messages:
_atexit_messages.add(message)
atexit.register(logger.warning, message)
class PerfControl(object):
"""Provides methods for setting the performance mode of a device."""
_AVAILABLE_GOVERNORS_REL_PATH = 'cpufreq/scaling_available_governors'
_CPU_FILE_PATTERN = re.compile(r'^cpu\d+$')
_CPU_PATH = '/sys/devices/system/cpu'
_KERNEL_MAX = '/sys/devices/system/cpu/kernel_max'
def __init__(self, device):
self._device = device
self._cpu_files = []
for file_name in self._device.ListDirectory(self._CPU_PATH, as_root=True):
if self._CPU_FILE_PATTERN.match(file_name):
self._cpu_files.append(file_name)
assert self._cpu_files, 'Failed to detect CPUs.'
self._cpu_file_list = ' '.join(self._cpu_files)
logger.info('CPUs found: %s', self._cpu_file_list)
self._have_mpdecision = self._device.FileExists('/system/bin/mpdecision')
raw = self._ReadEachCpuFile(self._AVAILABLE_GOVERNORS_REL_PATH)
self._available_governors = [
(cpu, raw_governors.strip().split() if not exit_code else None)
for cpu, raw_governors, exit_code in raw]
def _SetMaxFrequenciesFromMode(self, mode):
"""Set maximum frequencies for GPU and CPU cores.
Args:
mode: A dictionary mapping optional keys 'cpu_max_freq' and 'gpu_max_freq'
to integer values of frequency supported by the device.
"""
cpu_max_freq = mode.get('cpu_max_freq')
if cpu_max_freq:
if not isinstance(cpu_max_freq, dict):
self._SetScalingMaxFreqForCpus(cpu_max_freq, self._cpu_file_list)
else:
for key, max_frequency in cpu_max_freq.iteritems():
# Convert 'X' to 'cpuX' and 'X..Y' to 'cpuX cpu<X+1> .. cpuY'.
if '..' in key:
range_min, range_max = key.split('..')
range_min, range_max = int(range_min), int(range_max)
else:
range_min = range_max = int(key)
cpu_files = ['cpu%d' % number
for number in xrange(range_min, range_max + 1)]
# Set the |max_frequency| on requested subset of the cores.
self._SetScalingMaxFreqForCpus(max_frequency, ' '.join(cpu_files))
gpu_max_freq = mode.get('gpu_max_freq')
if gpu_max_freq:
self._SetMaxGpuClock(gpu_max_freq)
def SetHighPerfMode(self):
"""Sets the highest stable performance mode for the device."""
try:
self._device.EnableRoot()
except device_errors.CommandFailedError:
_NoisyWarning('Need root for performance mode.')
return
mode_definitions = _GetPerfModeDefinitions(self._device.product_model)
if not mode_definitions:
self.SetScalingGovernor('performance')
return
high_perf_mode = mode_definitions.get('high_perf_mode')
if not high_perf_mode:
self.SetScalingGovernor('performance')
return
if high_perf_mode.get('bring_cpu_cores_online', False):
self._ForceAllCpusOnline(True)
if not self._AllCpusAreOnline():
_NoisyWarning('Failed to force CPUs online.')
# Scaling governor must be set _after_ bringing all CPU cores online,
# otherwise it would not affect the cores that are currently offline.
self.SetScalingGovernor('performance')
self._SetMaxFrequenciesFromMode(high_perf_mode)
def SetDefaultPerfMode(self):
"""Sets the performance mode for the device to its default mode."""
if not self._device.HasRoot():
return
mode_definitions = _GetPerfModeDefinitions(self._device.product_model)
if not mode_definitions:
self.SetScalingGovernor('ondemand')
else:
default_mode_governor = mode_definitions.get('default_mode_governor')
assert default_mode_governor, ('Default mode governor must be provided '
'for all perf mode definitions.')
self.SetScalingGovernor(default_mode_governor)
default_mode = mode_definitions.get('default_mode')
if default_mode:
self._SetMaxFrequenciesFromMode(default_mode)
self._ForceAllCpusOnline(False)
def SetPerfProfilingMode(self):
"""Enables all cores for reliable perf profiling."""
self._ForceAllCpusOnline(True)
self.SetScalingGovernor('performance')
if not self._AllCpusAreOnline():
if not self._device.HasRoot():
raise RuntimeError('Need root to force CPUs online.')
raise RuntimeError('Failed to force CPUs online.')
def GetCpuInfo(self):
online = (output.rstrip() == '1' and status == 0
for (_, output, status) in self._ForEachCpu('cat "$CPU/online"'))
governor = (output.rstrip() if status == 0 else None
for (_, output, status)
in self._ForEachCpu('cat "$CPU/cpufreq/scaling_governor"'))
return zip(self._cpu_files, online, governor)
def _ForEachCpu(self, cmd, cpu_list=None):
"""Runs a command on the device for each of the CPUs.
Args:
cmd: A string with a shell command, may may use shell expansion: "$CPU" to
refer to the current CPU in the string form (e.g. "cpu0", "cpu1",
and so on).
cpu_list: A space-separated string of CPU core names, like in the example
above
Returns:
A list of tuples in the form (cpu_string, command_output, exit_code), one
tuple per each command invocation. As usual, all lines of the output
command are joined into one line with spaces.
"""
if cpu_list is None:
cpu_list = self._cpu_file_list
script = '; '.join([
'for CPU in %s' % cpu_list,
'do %s' % cmd,
'echo -n "%~%$?%~%"',
'done'
])
output = self._device.RunShellCommand(
script, cwd=self._CPU_PATH, check_return=True, as_root=True, shell=True)
output = '\n'.join(output).split('%~%')
return zip(self._cpu_files, output[0::2], (int(c) for c in output[1::2]))
def _ConditionallyWriteCpuFiles(self, path, value, cpu_files, condition):
template = (
'{condition} && test -e "$CPU/{path}" && echo {value} > "$CPU/{path}"')
results = self._ForEachCpu(
template.format(path=path, value=value, condition=condition), cpu_files)
cpus = ' '.join(cpu for (cpu, _, status) in results if status == 0)
if cpus:
logger.info('Successfully set %s to %r on: %s', path, value, cpus)
else:
logger.warning('Failed to set %s to %r on any cpus', path, value)
def _WriteCpuFiles(self, path, value, cpu_files):
self._ConditionallyWriteCpuFiles(path, value, cpu_files, condition='true')
def _ReadEachCpuFile(self, path):
return self._ForEachCpu(
'cat "$CPU/{path}"'.format(path=path))
def SetScalingGovernor(self, value):
"""Sets the scaling governor to the given value on all possible CPUs.
This does not attempt to set a governor to a value not reported as available
on the corresponding CPU.
Args:
value: [string] The new governor value.
"""
condition = 'test -e "{path}" && grep -q {value} {path}'.format(
path=('${CPU}/%s' % self._AVAILABLE_GOVERNORS_REL_PATH),
value=value)
self._ConditionallyWriteCpuFiles(
'cpufreq/scaling_governor', value, self._cpu_file_list, condition)
def GetScalingGovernor(self):
"""Gets the currently set governor for each CPU.
Returns:
An iterable of 2-tuples, each containing the cpu and the current
governor.
"""
raw = self._ReadEachCpuFile('cpufreq/scaling_governor')
return [
(cpu, raw_governor.strip() if not exit_code else None)
for cpu, raw_governor, exit_code in raw]
def ListAvailableGovernors(self):
"""Returns the list of available governors for each CPU.
Returns:
An iterable of 2-tuples, each containing the cpu and a list of available
governors for that cpu.
"""
return self._available_governors
def _SetScalingMaxFreqForCpus(self, value, cpu_files):
self._WriteCpuFiles('cpufreq/scaling_max_freq', '%d' % value, cpu_files)
def _SetMaxGpuClock(self, value):
self._device.WriteFile('/sys/class/kgsl/kgsl-3d0/max_gpuclk',
str(value),
as_root=True)
def _AllCpusAreOnline(self):
results = self._ForEachCpu('cat "$CPU/online"')
# The file 'cpu0/online' is missing on some devices (example: Nexus 9). This
# is likely because on these devices it is impossible to bring the cpu0
# offline. Assuming the same for all devices until proven otherwise.
return all(output.rstrip() == '1' and status == 0
for (cpu, output, status) in results
if cpu != 'cpu0')
def _ForceAllCpusOnline(self, force_online):
"""Enable all CPUs on a device.
Some vendors (or only Qualcomm?) hot-plug their CPUs, which can add noise
to measurements:
- In perf, samples are only taken for the CPUs that are online when the
measurement is started.
- The scaling governor can't be set for an offline CPU and frequency scaling
on newly enabled CPUs adds noise to both perf and tracing measurements.
It appears Qualcomm is the only vendor that hot-plugs CPUs, and on Qualcomm
this is done by "mpdecision".
"""
if self._have_mpdecision:
cmd = ['stop', 'mpdecision'] if force_online else ['start', 'mpdecision']
self._device.RunShellCommand(cmd, check_return=True, as_root=True)
if not self._have_mpdecision and not self._AllCpusAreOnline():
logger.warning('Unexpected cpu hot plugging detected.')
if force_online:
self._ForEachCpu('echo 1 > "$CPU/online"')

View File

@@ -0,0 +1,38 @@
# 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.
# pylint: disable=W0212
import os
import sys
import unittest
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from devil.android import device_test_case
from devil.android import device_utils
from devil.android.perf import perf_control
class TestPerfControl(device_test_case.DeviceTestCase):
def setUp(self):
super(TestPerfControl, self).setUp()
if not os.getenv('BUILDTYPE'):
os.environ['BUILDTYPE'] = 'Debug'
self._device = device_utils.DeviceUtils(self.serial)
def testHighPerfMode(self):
perf = perf_control.PerfControl(self._device)
try:
perf.SetPerfProfilingMode()
cpu_info = perf.GetCpuInfo()
self.assertEquals(len(perf._cpu_files), len(cpu_info))
for _, online, governor in cpu_info:
self.assertTrue(online)
self.assertEquals('performance', governor)
finally:
perf.SetDefaultPerfMode()
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,105 @@
# Copyright 2018 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 unittest
from devil import devil_env
from devil.android import device_utils
from devil.android.perf import perf_control
from devil.android.sdk import adb_wrapper
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock
# pylint: disable=unused-argument
def _ShellCommandHandler(cmd, shell=False, check_return=False,
cwd=None, env=None, run_as=None, as_root=False, single_line=False,
large_output=False, raw_output=False, timeout=None, retries=None):
if cmd.startswith('for CPU in '):
if 'scaling_available_governors' in cmd:
contents = 'interactive ondemand userspace powersave performance'
return [contents + '\n%~%0%~%'] * 4
if 'cat "$CPU/online"' in cmd:
return ['1\n%~%0%~%'] * 4
assert False, 'Should not be called with cmd: {}'.format(cmd)
class PerfControlTest(unittest.TestCase):
@staticmethod
def _MockOutLowLevelPerfControlMethods(perf_control_object):
# pylint: disable=protected-access
perf_control_object.SetScalingGovernor = mock.Mock()
perf_control_object._ForceAllCpusOnline = mock.Mock()
perf_control_object._SetScalingMaxFreqForCpus = mock.Mock()
perf_control_object._SetMaxGpuClock = mock.Mock()
# pylint: disable=no-self-use
def testNexus5HighPerfMode(self):
# Mock out the device state for PerfControl.
cpu_list = ['cpu%d' % cpu for cpu in xrange(4)]
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.product_model = 'Nexus 5'
mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
mock_device.ListDirectory.return_value = cpu_list + ['cpufreq']
mock_device.FileExists.return_value = True
mock_device.RunShellCommand = mock.Mock(side_effect=_ShellCommandHandler)
pc = perf_control.PerfControl(mock_device)
self._MockOutLowLevelPerfControlMethods(pc)
# Verify.
# pylint: disable=protected-access
# pylint: disable=no-member
pc.SetHighPerfMode()
mock_device.EnableRoot.assert_called_once_with()
pc._ForceAllCpusOnline.assert_called_once_with(True)
pc.SetScalingGovernor.assert_called_once_with('performance')
pc._SetScalingMaxFreqForCpus.assert_called_once_with(
1190400, ' '.join(cpu_list))
pc._SetMaxGpuClock.assert_called_once_with(200000000)
def testNexus5XHighPerfMode(self):
# Mock out the device state for PerfControl.
cpu_list = ['cpu%d' % cpu for cpu in xrange(6)]
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.product_model = 'Nexus 5X'
mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
mock_device.ListDirectory.return_value = cpu_list + ['cpufreq']
mock_device.FileExists.return_value = True
mock_device.RunShellCommand = mock.Mock(side_effect=_ShellCommandHandler)
pc = perf_control.PerfControl(mock_device)
self._MockOutLowLevelPerfControlMethods(pc)
# Verify.
# pylint: disable=protected-access
# pylint: disable=no-member
pc.SetHighPerfMode()
mock_device.EnableRoot.assert_called_once_with()
pc._ForceAllCpusOnline.assert_called_once_with(True)
pc.SetScalingGovernor.assert_called_once_with('performance')
pc._SetScalingMaxFreqForCpus.assert_called_once_with(
1248000, ' '.join(cpu_list))
pc._SetMaxGpuClock.assert_called_once_with(300000000)
def testNexus5XDefaultPerfMode(self):
# Mock out the device state for PerfControl.
cpu_list = ['cpu%d' % cpu for cpu in xrange(6)]
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.product_model = 'Nexus 5X'
mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
mock_device.ListDirectory.return_value = cpu_list + ['cpufreq']
mock_device.FileExists.return_value = True
mock_device.RunShellCommand = mock.Mock(side_effect=_ShellCommandHandler)
pc = perf_control.PerfControl(mock_device)
self._MockOutLowLevelPerfControlMethods(pc)
# Verify.
# pylint: disable=protected-access
# pylint: disable=no-member
pc.SetDefaultPerfMode()
pc.SetScalingGovernor.assert_called_once_with('ondemand')
pc._SetScalingMaxFreqForCpus.assert_any_call(1440000, 'cpu0 cpu1 cpu2 cpu3')
pc._SetScalingMaxFreqForCpus.assert_any_call(1824000, 'cpu4 cpu5')
pc._SetMaxGpuClock.assert_called_once_with(600000000)
pc._ForceAllCpusOnline.assert_called_once_with(False)

View File

@@ -0,0 +1,209 @@
# 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.
import logging
import Queue
import re
import threading
# Log marker containing SurfaceTexture timestamps.
_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps'
_SURFACE_TEXTURE_TIMESTAMP_RE = r'\d+'
class SurfaceStatsCollector(object):
"""Collects surface stats for a SurfaceView from the output of SurfaceFlinger.
Args:
device: A DeviceUtils instance.
"""
def __init__(self, device):
self._device = device
self._collector_thread = None
self._surface_before = None
self._get_data_event = None
self._data_queue = None
self._stop_event = None
self._warn_about_empty_data = True
def DisableWarningAboutEmptyData(self):
self._warn_about_empty_data = False
def Start(self):
assert not self._collector_thread
if self._ClearSurfaceFlingerLatencyData():
self._get_data_event = threading.Event()
self._stop_event = threading.Event()
self._data_queue = Queue.Queue()
self._collector_thread = threading.Thread(target=self._CollectorThread)
self._collector_thread.start()
else:
raise Exception('SurfaceFlinger not supported on this device.')
def Stop(self):
assert self._collector_thread
(refresh_period, timestamps) = self._GetDataFromThread()
if self._collector_thread:
self._stop_event.set()
self._collector_thread.join()
self._collector_thread = None
return (refresh_period, timestamps)
def _CollectorThread(self):
last_timestamp = 0
timestamps = []
retries = 0
while not self._stop_event.is_set():
self._get_data_event.wait(1)
try:
refresh_period, new_timestamps = self._GetSurfaceFlingerFrameData()
if refresh_period is None or timestamps is None:
retries += 1
if retries < 3:
continue
if last_timestamp:
# Some data has already been collected, but either the app
# was closed or there's no new data. Signal the main thread and
# wait.
self._data_queue.put((None, None))
self._stop_event.wait()
break
raise Exception('Unable to get surface flinger latency data')
timestamps += [timestamp for timestamp in new_timestamps
if timestamp > last_timestamp]
if len(timestamps):
last_timestamp = timestamps[-1]
if self._get_data_event.is_set():
self._get_data_event.clear()
self._data_queue.put((refresh_period, timestamps))
timestamps = []
except Exception as e:
# On any error, before aborting, put the exception into _data_queue to
# prevent the main thread from waiting at _data_queue.get() infinitely.
self._data_queue.put(e)
raise
def _GetDataFromThread(self):
self._get_data_event.set()
ret = self._data_queue.get()
if isinstance(ret, Exception):
raise ret
return ret
def _ClearSurfaceFlingerLatencyData(self):
"""Clears the SurfaceFlinger latency data.
Returns:
True if SurfaceFlinger latency is supported by the device, otherwise
False.
"""
# The command returns nothing if it is supported, otherwise returns many
# lines of result just like 'dumpsys SurfaceFlinger'.
results = self._device.RunShellCommand(
['dumpsys', 'SurfaceFlinger', '--latency-clear', 'SurfaceView'],
check_return=True)
return not len(results)
def GetSurfaceFlingerPid(self):
try:
# Returns the first matching PID found.
return next(p.pid for p in self._device.ListProcesses('surfaceflinger'))
except StopIteration:
raise Exception('Unable to get surface flinger process id')
def _GetSurfaceViewWindowName(self):
results = self._device.RunShellCommand(
['dumpsys', 'SurfaceFlinger', '--list'], check_return=True)
for window_name in results:
if window_name.startswith('SurfaceView'):
return window_name
return None
def _GetSurfaceFlingerFrameData(self):
"""Returns collected SurfaceFlinger frame timing data.
Returns:
A tuple containing:
- The display's nominal refresh period in milliseconds.
- A list of timestamps signifying frame presentation times in
milliseconds.
The return value may be (None, None) if there was no data collected (for
example, if the app was closed before the collector thread has finished).
"""
window_name = self._GetSurfaceViewWindowName()
command = ['dumpsys', 'SurfaceFlinger', '--latency']
# Even if we don't find the window name, run the command to get the refresh
# period.
if window_name:
command.append(window_name)
output = self._device.RunShellCommand(command, check_return=True)
return ParseFrameData(output, parse_timestamps=bool(window_name))
def ParseFrameData(lines, parse_timestamps):
# adb shell dumpsys SurfaceFlinger --latency <window name>
# prints some information about the last 128 frames displayed in
# that window.
# The data returned looks like this:
# 16954612
# 7657467895508 7657482691352 7657493499756
# 7657484466553 7657499645964 7657511077881
# 7657500793457 7657516600576 7657527404785
# (...)
#
# The first line is the refresh period (here 16.95 ms), it is followed
# by 128 lines w/ 3 timestamps in nanosecond each:
# A) when the app started to draw
# B) the vsync immediately preceding SF submitting the frame to the h/w
# C) timestamp immediately after SF submitted that frame to the h/w
#
# The difference between the 1st and 3rd timestamp is the frame-latency.
# An interesting data is when the frame latency crosses a refresh period
# boundary, this can be calculated this way:
#
# ceil((C - A) / refresh-period)
#
# (each time the number above changes, we have a "jank").
# If this happens a lot during an animation, the animation appears
# janky, even if it runs at 60 fps in average.
results = []
for line in lines:
# Skip over lines with anything other than digits and whitespace.
if re.search(r'[^\d\s]', line):
logging.warning('unexpected output: %s', line)
else:
results.append(line)
if not results:
return None, None
timestamps = []
nanoseconds_per_millisecond = 1e6
refresh_period = long(results[0]) / nanoseconds_per_millisecond
if not parse_timestamps:
return refresh_period, timestamps
# If a fence associated with a frame is still pending when we query the
# latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
# Since we only care about completed frames, we will ignore any timestamps
# with this value.
pending_fence_timestamp = (1 << 63) - 1
for line in results[1:]:
fields = line.split()
if len(fields) != 3:
logging.warning('Unexpected line: %s', line)
continue
timestamp = long(fields[1])
if timestamp == pending_fence_timestamp:
continue
timestamp /= nanoseconds_per_millisecond
timestamps.append(timestamp)
return refresh_period, timestamps

View File

@@ -0,0 +1,40 @@
# Copyright 2019 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 unittest
from devil.android.perf import surface_stats_collector
class SurfaceStatsCollectorTests(unittest.TestCase):
def testParseFrameData_simple(self):
actual = surface_stats_collector.ParseFrameData([
'16954612',
'7657467895508 7657482691352 7657493499756',
'7657484466553 7657499645964 7657511077881',
'7657500793457 7657516600576 7657527404785',
], parse_timestamps=True)
self.assertEqual(
actual, (16.954612, [7657482.691352, 7657499.645964, 7657516.600576]))
def testParseFrameData_withoutTimestamps(self):
actual = surface_stats_collector.ParseFrameData([
'16954612',
'7657467895508 7657482691352 7657493499756',
'7657484466553 7657499645964 7657511077881',
'7657500793457 7657516600576 7657527404785',
], parse_timestamps=False)
self.assertEqual(
actual, (16.954612, []))
def testParseFrameData_withWarning(self):
actual = surface_stats_collector.ParseFrameData([
'SurfaceFlinger appears to be unresponsive, dumping anyways',
'16954612',
'7657467895508 7657482691352 7657493499756',
'7657484466553 7657499645964 7657511077881',
'7657500793457 7657516600576 7657527404785',
], parse_timestamps=True)
self.assertEqual(
actual, (16.954612, [7657482.691352, 7657499.645964, 7657516.600576]))

View File

@@ -0,0 +1,136 @@
# 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.
import logging
logger = logging.getLogger(__name__)
class OmapThrottlingDetector(object):
"""Class to detect and track thermal throttling on an OMAP 4."""
OMAP_TEMP_FILE = ('/sys/devices/platform/omap/omap_temp_sensor.0/'
'temperature')
@staticmethod
def IsSupported(device):
return device.FileExists(OmapThrottlingDetector.OMAP_TEMP_FILE)
def __init__(self, device):
self._device = device
@staticmethod
def BecameThrottled(log_line):
return 'omap_thermal_throttle' in log_line
@staticmethod
def BecameUnthrottled(log_line):
return 'omap_thermal_unthrottle' in log_line
@staticmethod
def GetThrottlingTemperature(log_line):
if 'throttle_delayed_work_fn' in log_line:
return float([s for s in log_line.split() if s.isdigit()][0]) / 1000.0
def GetCurrentTemperature(self):
tempdata = self._device.ReadFile(OmapThrottlingDetector.OMAP_TEMP_FILE)
return float(tempdata) / 1000.0
class ExynosThrottlingDetector(object):
"""Class to detect and track thermal throttling on an Exynos 5."""
@staticmethod
def IsSupported(device):
return device.FileExists('/sys/bus/exynos5-core')
def __init__(self, device):
pass
@staticmethod
def BecameThrottled(log_line):
return 'exynos_tmu: Throttling interrupt' in log_line
@staticmethod
def BecameUnthrottled(log_line):
return 'exynos_thermal_unthrottle: not throttling' in log_line
@staticmethod
def GetThrottlingTemperature(_log_line):
return None
@staticmethod
def GetCurrentTemperature():
return None
class ThermalThrottle(object):
"""Class to detect and track thermal throttling.
Usage:
Wait for IsThrottled() to be False before running test
After running test call HasBeenThrottled() to find out if the
test run was affected by thermal throttling.
"""
def __init__(self, device):
self._device = device
self._throttled = False
self._detector = None
# pylint: disable=redefined-variable-type
if OmapThrottlingDetector.IsSupported(device):
self._detector = OmapThrottlingDetector(device)
elif ExynosThrottlingDetector.IsSupported(device):
self._detector = ExynosThrottlingDetector(device)
def HasBeenThrottled(self):
"""True if there has been any throttling since the last call to
HasBeenThrottled or IsThrottled.
"""
return self._ReadLog()
def IsThrottled(self):
"""True if currently throttled."""
self._ReadLog()
return self._throttled
def _ReadLog(self):
if not self._detector:
return False
has_been_throttled = False
serial_number = str(self._device)
log = self._device.RunShellCommand(
['dmesg', '-c'], large_output=True, check_return=True)
degree_symbol = unichr(0x00B0)
for line in log:
if self._detector.BecameThrottled(line):
if not self._throttled:
logger.warning('>>> Device %s thermally throttled', serial_number)
self._throttled = True
has_been_throttled = True
elif self._detector.BecameUnthrottled(line):
if self._throttled:
logger.warning('>>> Device %s thermally unthrottled', serial_number)
self._throttled = False
has_been_throttled = True
temperature = self._detector.GetThrottlingTemperature(line)
if temperature is not None:
logger.info(u'Device %s thermally throttled at %3.1f%sC',
serial_number, temperature, degree_symbol)
if logger.isEnabledFor(logging.DEBUG):
# Print current temperature of CPU SoC.
temperature = self._detector.GetCurrentTemperature()
if temperature is not None:
logger.debug(u'Current SoC temperature of %s = %3.1f%sC',
serial_number, temperature, degree_symbol)
# Print temperature of battery, to give a system temperature
dumpsys_log = self._device.RunShellCommand(
['dumpsys', 'battery'], check_return=True)
for line in dumpsys_log:
if 'temperature' in line:
btemp = float([s for s in line.split() if s.isdigit()][0]) / 10.0
logger.debug(u'Current battery temperature of %s = %3.1f%sC',
serial_number, btemp, degree_symbol)
return has_been_throttled

View File

@@ -0,0 +1,178 @@
# 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.
"""Functions that deal with local and device ports."""
import contextlib
import fcntl
import httplib
import logging
import os
import socket
import traceback
logger = logging.getLogger(__name__)
# The net test server is started from port 10201.
_TEST_SERVER_PORT_FIRST = 10201
_TEST_SERVER_PORT_LAST = 30000
# A file to record next valid port of test server.
_TEST_SERVER_PORT_FILE = '/tmp/test_server_port'
_TEST_SERVER_PORT_LOCKFILE = '/tmp/test_server_port.lock'
# The following two methods are used to allocate the port source for various
# types of test servers. Because some net-related tests can be run on shards at
# same time, it's important to have a mechanism to allocate the port
# process-safe. In here, we implement the safe port allocation by leveraging
# flock.
def ResetTestServerPortAllocation():
"""Resets the port allocation to start from TEST_SERVER_PORT_FIRST.
Returns:
Returns True if reset successes. Otherwise returns False.
"""
try:
with open(_TEST_SERVER_PORT_FILE, 'w') as fp:
fp.write('%d' % _TEST_SERVER_PORT_FIRST)
return True
except Exception: # pylint: disable=broad-except
logger.exception('Error while resetting port allocation')
return False
def AllocateTestServerPort():
"""Allocates a port incrementally.
Returns:
Returns a valid port which should be in between TEST_SERVER_PORT_FIRST and
TEST_SERVER_PORT_LAST. Returning 0 means no more valid port can be used.
"""
port = 0
ports_tried = []
try:
fp_lock = open(_TEST_SERVER_PORT_LOCKFILE, 'w')
fcntl.flock(fp_lock, fcntl.LOCK_EX)
# Get current valid port and calculate next valid port.
if not os.path.exists(_TEST_SERVER_PORT_FILE):
ResetTestServerPortAllocation()
with open(_TEST_SERVER_PORT_FILE, 'r+') as fp:
port = int(fp.read())
ports_tried.append(port)
while not IsHostPortAvailable(port):
port += 1
ports_tried.append(port)
if (port > _TEST_SERVER_PORT_LAST or
port < _TEST_SERVER_PORT_FIRST):
port = 0
else:
fp.seek(0, os.SEEK_SET)
fp.write('%d' % (port + 1))
except Exception: # pylint: disable=broad-except
logger.exception('Error while allocating port')
finally:
if fp_lock:
fcntl.flock(fp_lock, fcntl.LOCK_UN)
fp_lock.close()
if port:
logger.info('Allocate port %d for test server.', port)
else:
logger.error('Could not allocate port for test server. '
'List of ports tried: %s', str(ports_tried))
return port
def IsHostPortAvailable(host_port):
"""Checks whether the specified host port is available.
Args:
host_port: Port on host to check.
Returns:
True if the port on host is available, otherwise returns False.
"""
s = socket.socket()
try:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', host_port))
s.close()
return True
except socket.error:
return False
def IsDevicePortUsed(device, device_port, state=''):
"""Checks whether the specified device port is used or not.
Args:
device: A DeviceUtils instance.
device_port: Port on device we want to check.
state: String of the specified state. Default is empty string, which
means any state.
Returns:
True if the port on device is already used, otherwise returns False.
"""
base_urls = ('127.0.0.1:%d' % device_port, 'localhost:%d' % device_port)
netstat_results = device.RunShellCommand(
['netstat', '-an'], check_return=True, large_output=True)
for single_connect in netstat_results:
# Column 3 is the local address which we want to check with.
connect_results = single_connect.split()
if connect_results[0] != 'tcp':
continue
if len(connect_results) < 6:
raise Exception('Unexpected format while parsing netstat line: ' +
single_connect)
is_state_match = connect_results[5] == state if state else True
if connect_results[3] in base_urls and is_state_match:
return True
return False
def IsHttpServerConnectable(host, port, tries=3, command='GET', path='/',
expected_read='', timeout=2):
"""Checks whether the specified http server is ready to serve request or not.
Args:
host: Host name of the HTTP server.
port: Port number of the HTTP server.
tries: How many times we want to test the connection. The default value is
3.
command: The http command we use to connect to HTTP server. The default
command is 'GET'.
path: The path we use when connecting to HTTP server. The default path is
'/'.
expected_read: The content we expect to read from the response. The default
value is ''.
timeout: Timeout (in seconds) for each http connection. The default is 2s.
Returns:
Tuple of (connect status, client error). connect status is a boolean value
to indicate whether the server is connectable. client_error is the error
message the server returns when connect status is false.
"""
assert tries >= 1
for i in xrange(0, tries):
client_error = None
try:
with contextlib.closing(httplib.HTTPConnection(
host, port, timeout=timeout)) as http:
# Output some debug information when we have tried more than 2 times.
http.set_debuglevel(i >= 2)
http.request(command, path)
r = http.getresponse()
content = r.read()
if r.status == 200 and r.reason == 'OK' and content == expected_read:
return (True, '')
client_error = ('Bad response: %s %s version %s\n ' %
(r.status, r.reason, r.version) +
'\n '.join([': '.join(h) for h in r.getheaders()]))
except (httplib.HTTPException, socket.error) as e:
# Probably too quick connecting: try again.
exception_error_msgs = traceback.format_exception_only(type(e), e)
if exception_error_msgs:
client_error = ''.join(exception_error_msgs)
# Only returns last client_error.
return (False, client_error or 'Timeout')

View File

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

View File

@@ -0,0 +1,43 @@
# 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."""
from devil.android.sdk import build_tools
from devil.utils import cmd_helper
from devil.utils import lazy
_aapt_path = lazy.WeakConstant(lambda: build_tools.GetPath('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.read()] + 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()

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python
# Copyright 2016 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 contextlib
import os
import posixpath
import random
import signal
import sys
import unittest
_CATAPULT_BASE_DIR = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..', '..'))
sys.path.append(os.path.join(_CATAPULT_BASE_DIR, 'devil'))
from devil import devil_env
from devil.android import device_errors
from devil.android import device_test_case
from devil.android.sdk import adb_wrapper
from devil.utils import cmd_helper
from devil.utils import timeout_retry
_TEST_DATA_DIR = os.path.abspath(os.path.join(
os.path.dirname(__file__), 'test', 'data'))
def _hostAdbPids():
ps_status, ps_output = cmd_helper.GetCmdStatusAndOutput(
['pgrep', '-l', 'adb'])
if ps_status != 0:
return []
pids_and_names = (line.split() for line in ps_output.splitlines())
return [int(pid) for pid, name in pids_and_names
if name == 'adb']
class AdbCompatibilityTest(device_test_case.DeviceTestCase):
@classmethod
def setUpClass(cls):
custom_adb_path = os.environ.get('ADB_PATH')
custom_deps = {
'config_type': 'BaseConfig',
'dependencies': {},
}
if custom_adb_path:
custom_deps['dependencies']['adb'] = {
'file_info': {
devil_env.GetPlatform(): {
'local_paths': [custom_adb_path],
},
},
}
devil_env.config.Initialize(configs=[custom_deps])
def testStartServer(self):
# Manually kill off any instances of adb.
adb_pids = _hostAdbPids()
for p in adb_pids:
os.kill(p, signal.SIGKILL)
self.assertIsNotNone(
timeout_retry.WaitFor(
lambda: not _hostAdbPids(), wait_period=0.1, max_tries=10))
# start the adb server
start_server_status, _ = cmd_helper.GetCmdStatusAndOutput(
[adb_wrapper.AdbWrapper.GetAdbPath(), 'start-server'])
# verify that the server is now online
self.assertEquals(0, start_server_status)
self.assertIsNotNone(
timeout_retry.WaitFor(
lambda: bool(_hostAdbPids()), wait_period=0.1, max_tries=10))
def testKillServer(self):
adb_pids = _hostAdbPids()
if not adb_pids:
adb_wrapper.AdbWrapper.StartServer()
adb_pids = _hostAdbPids()
self.assertGreaterEqual(len(adb_pids), 1)
kill_server_status, _ = cmd_helper.GetCmdStatusAndOutput(
[adb_wrapper.AdbWrapper.GetAdbPath(), 'kill-server'])
self.assertEqual(0, kill_server_status)
adb_pids = _hostAdbPids()
self.assertEqual(0, len(adb_pids))
def testDevices(self):
devices = adb_wrapper.AdbWrapper.Devices()
self.assertNotEqual(0, len(devices), 'No devices found.')
def getTestInstance(self):
"""Creates a real AdbWrapper instance for testing."""
return adb_wrapper.AdbWrapper(self.serial)
def testShell(self):
under_test = self.getTestInstance()
shell_ls_result = under_test.Shell('ls')
self.assertIsInstance(shell_ls_result, str)
self.assertTrue(bool(shell_ls_result))
def testShell_failed(self):
under_test = self.getTestInstance()
with self.assertRaises(device_errors.AdbShellCommandFailedError):
under_test.Shell('ls /foo/bar/baz')
def testShell_externalStorageDefined(self):
under_test = self.getTestInstance()
external_storage = under_test.Shell('echo $EXTERNAL_STORAGE')
self.assertIsInstance(external_storage, str)
self.assertTrue(posixpath.isabs(external_storage))
@contextlib.contextmanager
def getTestPushDestination(self, under_test):
"""Creates a temporary directory suitable for pushing to."""
external_storage = under_test.Shell('echo $EXTERNAL_STORAGE').strip()
if not external_storage:
self.skipTest('External storage not available.')
while True:
random_hex = hex(random.randint(0, 2 ** 52))[2:]
name = 'tmp_push_test%s' % random_hex
path = posixpath.join(external_storage, name)
try:
under_test.Shell('ls %s' % path)
except device_errors.AdbShellCommandFailedError:
break
under_test.Shell('mkdir %s' % path)
try:
yield path
finally:
under_test.Shell('rm -rf %s' % path)
def testPush_fileToFile(self):
under_test = self.getTestInstance()
with self.getTestPushDestination(under_test) as push_target_directory:
src = os.path.join(_TEST_DATA_DIR, 'push_file.txt')
dest = posixpath.join(push_target_directory, 'push_file.txt')
with self.assertRaises(device_errors.AdbShellCommandFailedError):
under_test.Shell('ls %s' % dest)
under_test.Push(src, dest)
self.assertEquals(dest, under_test.Shell('ls %s' % dest).strip())
def testPush_fileToDirectory(self):
under_test = self.getTestInstance()
with self.getTestPushDestination(under_test) as push_target_directory:
src = os.path.join(_TEST_DATA_DIR, 'push_file.txt')
dest = push_target_directory
resulting_file = posixpath.join(dest, 'push_file.txt')
with self.assertRaises(device_errors.AdbShellCommandFailedError):
under_test.Shell('ls %s' % resulting_file)
under_test.Push(src, dest)
self.assertEquals(
resulting_file,
under_test.Shell('ls %s' % resulting_file).strip())
def testPush_directoryToDirectory(self):
under_test = self.getTestInstance()
with self.getTestPushDestination(under_test) as push_target_directory:
src = os.path.join(_TEST_DATA_DIR, 'push_directory')
dest = posixpath.join(push_target_directory, 'push_directory')
with self.assertRaises(device_errors.AdbShellCommandFailedError):
under_test.Shell('ls %s' % dest)
under_test.Push(src, dest)
self.assertEquals(
sorted(os.listdir(src)),
sorted(under_test.Shell('ls %s' % dest).strip().split()))
def testPush_directoryToExistingDirectory(self):
under_test = self.getTestInstance()
with self.getTestPushDestination(under_test) as push_target_directory:
src = os.path.join(_TEST_DATA_DIR, 'push_directory')
dest = push_target_directory
resulting_directory = posixpath.join(dest, 'push_directory')
with self.assertRaises(device_errors.AdbShellCommandFailedError):
under_test.Shell('ls %s' % resulting_directory)
under_test.Shell('mkdir %s' % resulting_directory)
under_test.Push(src, dest)
self.assertEquals(
sorted(os.listdir(src)),
sorted(under_test.Shell('ls %s' % resulting_directory).split()))
# TODO(jbudorick): Implement tests for the following:
# taskset -c
# devices [-l]
# pull
# shell
# ls
# logcat [-c] [-d] [-v] [-b]
# forward [--remove] [--list]
# jdwp
# install [-l] [-r] [-s] [-d]
# install-multiple [-l] [-r] [-s] [-d] [-p]
# uninstall [-k]
# backup -f [-apk] [-shared] [-nosystem] [-all]
# restore
# wait-for-device
# get-state (BROKEN IN THE M SDK)
# get-devpath
# remount
# reboot
# reboot-bootloader
# root
# emu
@classmethod
def tearDownClass(cls):
print
print
print 'tested %s' % adb_wrapper.AdbWrapper.GetAdbPath()
print ' %s' % adb_wrapper.AdbWrapper.Version()
print 'connected devices:'
try:
for d in adb_wrapper.AdbWrapper.Devices():
print ' %s' % d
except device_errors.AdbCommandFailedError:
print ' <failed to list devices>'
raise
finally:
print
if __name__ == '__main__':
sys.exit(unittest.main())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python
# 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.
"""Tests for the AdbWrapper class."""
import os
import tempfile
import time
import unittest
from devil.android import device_test_case
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
class TestAdbWrapper(device_test_case.DeviceTestCase):
def setUp(self):
super(TestAdbWrapper, self).setUp()
self._adb = adb_wrapper.AdbWrapper(self.serial)
self._adb.WaitForDevice()
@staticmethod
def _MakeTempFile(contents):
"""Make a temporary file with the given contents.
Args:
contents: string to write to the temporary file.
Returns:
The absolute path to the file.
"""
fi, path = tempfile.mkstemp()
with os.fdopen(fi, 'wb') as f:
f.write(contents)
return path
def testDeviceUnreachable(self):
with self.assertRaises(device_errors.DeviceUnreachableError):
bad_adb = adb_wrapper.AdbWrapper('device_gone')
bad_adb.Shell('echo test')
def testShell(self):
output = self._adb.Shell('echo test', expect_status=0)
self.assertEqual(output.strip(), 'test')
output = self._adb.Shell('echo test')
self.assertEqual(output.strip(), 'test')
with self.assertRaises(device_errors.AdbCommandFailedError):
self._adb.Shell('echo test', expect_status=1)
def testPersistentShell(self):
# We need to access the device serial number here in order
# to create the persistent shell.
serial = self._adb.GetDeviceSerial() # pylint: disable=protected-access
with self._adb.PersistentShell(serial) as pshell:
(res1, code1) = pshell.RunCommand('echo TEST')
(res2, code2) = pshell.RunCommand('echo TEST2')
self.assertEqual(len(res1), 1)
self.assertEqual(res1[0], 'TEST')
self.assertEqual(res2[-1], 'TEST2')
self.assertEqual(code1, 0)
self.assertEqual(code2, 0)
def testPushLsPull(self):
path = self._MakeTempFile('foo')
device_path = '/data/local/tmp/testfile.txt'
local_tmpdir = os.path.dirname(path)
self._adb.Push(path, device_path)
files = dict(self._adb.Ls('/data/local/tmp'))
self.assertTrue('testfile.txt' in files)
self.assertEquals(3, files['testfile.txt'].st_size)
self.assertEqual(self._adb.Shell('cat %s' % device_path), 'foo')
self._adb.Pull(device_path, local_tmpdir)
with open(os.path.join(local_tmpdir, 'testfile.txt'), 'r') as f:
self.assertEqual(f.read(), 'foo')
def testInstall(self):
path = self._MakeTempFile('foo')
with self.assertRaises(device_errors.AdbCommandFailedError):
self._adb.Install(path)
def testForward(self):
with self.assertRaises(device_errors.AdbCommandFailedError):
self._adb.Forward(0, 0)
def testUninstall(self):
with self.assertRaises(device_errors.AdbCommandFailedError):
self._adb.Uninstall('some.nonexistant.package')
def testRebootWaitForDevice(self):
self._adb.Reboot()
print 'waiting for device to reboot...'
while self._adb.GetState() == 'device':
time.sleep(1)
self._adb.WaitForDevice()
self.assertEqual(self._adb.GetState(), 'device')
print 'waiting for package manager...'
while True:
try:
android_path = self._adb.Shell('pm path android')
except device_errors.AdbShellCommandFailedError:
android_path = None
if android_path and 'package:' in android_path:
break
time.sleep(1)
def testRootRemount(self):
self._adb.Root()
while True:
try:
self._adb.Shell('start')
break
except device_errors.DeviceUnreachableError:
time.sleep(1)
self._adb.Remount()
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python
# Copyright 2017 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.
"""
Unit tests for some APIs with conditional logic in adb_wrapper.py
"""
import unittest
from devil import devil_env
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
class AdbWrapperTest(unittest.TestCase):
def setUp(self):
self.device_serial = 'ABC12345678'
self.adb = adb_wrapper.AdbWrapper(self.device_serial)
def _MockRunDeviceAdbCmd(self, return_value):
return mock.patch.object(
self.adb,
'_RunDeviceAdbCmd',
mock.Mock(side_effect=None, return_value=return_value))
def testDisableVerityWhenDisabled(self):
with self._MockRunDeviceAdbCmd('Verity already disabled on /system'):
self.adb.DisableVerity()
def testDisableVerityWhenEnabled(self):
with self._MockRunDeviceAdbCmd(
'Verity disabled on /system\nNow reboot your device for settings to '
'take effect'):
self.adb.DisableVerity()
def testEnableVerityWhenEnabled(self):
with self._MockRunDeviceAdbCmd('Verity already enabled on /system'):
self.adb.EnableVerity()
def testEnableVerityWhenDisabled(self):
with self._MockRunDeviceAdbCmd(
'Verity enabled on /system\nNow reboot your device for settings to '
'take effect'):
self.adb.EnableVerity()
def testFailEnableVerity(self):
with self._MockRunDeviceAdbCmd('error: closed'):
self.assertRaises(
device_errors.AdbCommandFailedError, self.adb.EnableVerity)
def testFailDisableVerity(self):
with self._MockRunDeviceAdbCmd('error: closed'):
self.assertRaises(
device_errors.AdbCommandFailedError, self.adb.DisableVerity)
@mock.patch('devil.utils.cmd_helper.GetCmdStatusAndOutputWithTimeout')
def testDeviceUnreachable(self, get_cmd_mock):
get_cmd_mock.return_value = (
1, "error: device '%s' not found" % self.device_serial)
self.assertRaises(
device_errors.DeviceUnreachableError, self.adb.Shell, '/bin/true')
@mock.patch('devil.utils.cmd_helper.GetCmdStatusAndOutputWithTimeout')
def testWaitingForDevice(self, get_cmd_mock):
get_cmd_mock.return_value = (1, '- waiting for device - ')
self.assertRaises(
device_errors.DeviceUnreachableError, self.adb.Shell, '/bin/true')

View File

@@ -0,0 +1,51 @@
# 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 import devil_env
from devil.utils import lazy
with devil_env.SysPath(devil_env.DEPENDENCY_MANAGER_PATH):
import dependency_manager # pylint: disable=import-error
def GetPath(build_tool):
try:
return devil_env.config.LocalPath(build_tool)
except dependency_manager.NoPathFoundError:
pass
try:
return _PathInLocalSdk(build_tool)
except dependency_manager.NoPathFoundError:
pass
return devil_env.config.FetchPath(build_tool)
def _PathInLocalSdk(build_tool):
build_tools_path = _build_tools_path.read()
return (os.path.join(build_tools_path, build_tool) if build_tools_path
else None)
def _FindBuildTools():
android_sdk_path = devil_env.config.LocalPath('android_sdk')
if not android_sdk_path:
return None
build_tools_contents = os.listdir(
os.path.join(android_sdk_path, 'build-tools'))
if not build_tools_contents:
return None
else:
if len(build_tools_contents) > 1:
build_tools_contents.sort()
return os.path.join(android_sdk_path, 'build-tools',
build_tools_contents[-1])
_build_tools_path = lazy.WeakConstant(_FindBuildTools)

View File

@@ -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.
from devil.android.sdk import build_tools
from devil.utils import cmd_helper
from devil.utils import lazy
_dexdump_path = lazy.WeakConstant(lambda: build_tools.GetPath('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.read()] + dexfiles
if file_summary:
args.append('-f')
return cmd_helper.IterCmdOutputLines(args)

View File

@@ -0,0 +1,122 @@
# 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 fastboot tool.
This is a thin wrapper around the fastboot interface. Any additional complexity
should be delegated to a higher level (ex. FastbootUtils).
"""
# pylint: disable=unused-argument
from devil import devil_env
from devil.android import decorators
from devil.android import device_errors
from devil.utils import cmd_helper
from devil.utils import lazy
_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3
_FLASH_TIMEOUT = _DEFAULT_TIMEOUT * 10
class Fastboot(object):
_fastboot_path = lazy.WeakConstant(
lambda: devil_env.config.FetchPath('fastboot'))
def __init__(self, device_serial, default_timeout=_DEFAULT_TIMEOUT,
default_retries=_DEFAULT_RETRIES):
"""Initializes the FastbootWrapper.
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)
self._default_timeout = default_timeout
self._default_retries = default_retries
def __str__(self):
return self._device_serial
@classmethod
def _RunFastbootCommand(cls, cmd):
"""Run a generic fastboot command.
Args:
cmd: Command to run. Must be list of args, the first one being the command
Returns:
output of command.
Raises:
TypeError: If cmd is not of type list.
"""
if isinstance(cmd, list):
cmd = [cls._fastboot_path.read()] + cmd
else:
raise TypeError(
'Command for _RunDeviceFastbootCommand must be a list.')
status, output = cmd_helper.GetCmdStatusAndOutput(cmd)
if int(status) != 0:
raise device_errors.FastbootCommandFailedError(cmd, output, status)
return output
def _RunDeviceFastbootCommand(self, cmd):
"""Run a fastboot command on the device associated with this object.
Args:
cmd: Command to run. Must be list of args, the first one being the command
Returns:
output of command.
Raises:
TypeError: If cmd is not of type list.
"""
if isinstance(cmd, list):
cmd = ['-s', self._device_serial] + cmd
return self._RunFastbootCommand(cmd)
@decorators.WithTimeoutAndRetriesDefaults(_FLASH_TIMEOUT, 0)
def Flash(self, partition, image, timeout=None, retries=None):
"""Flash partition with img.
Args:
partition: Partition to be flashed.
image: location of image to flash with.
"""
self._RunDeviceFastbootCommand(['flash', partition, image])
@classmethod
@decorators.WithTimeoutAndRetriesDefaults(_DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
def Devices(cls, timeout=None, retries=None):
"""Outputs list of devices in fastboot mode.
Returns:
List of Fastboot objects, one for each device in fastboot.
"""
output = cls._RunFastbootCommand(['devices'])
return [Fastboot(line.split()[0]) for line in output.splitlines()]
@decorators.WithTimeoutAndRetriesFromInstance()
def RebootBootloader(self, timeout=None, retries=None):
"""Reboot from fastboot, into fastboot."""
self._RunDeviceFastbootCommand(['reboot-bootloader'])
@decorators.WithTimeoutAndRetriesDefaults(_FLASH_TIMEOUT, 0)
def Reboot(self, timeout=None, retries=None):
"""Reboot from fastboot to normal usage"""
self._RunDeviceFastbootCommand(['reboot'])
@decorators.WithTimeoutAndRetriesFromInstance()
def SetOemOffModeCharge(self, value, timeout=None, retries=None):
"""Sets off mode charging
Args:
value: boolean value to set off-mode-charging on or off.
"""
self._RunDeviceFastbootCommand(
['oem', 'off-mode-charge', str(int(value))])

View File

@@ -0,0 +1,154 @@
# 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 work around for various adb commands on android gce instances.
Some adb commands don't work well when the device is a cloud vm, namely
'push' and 'pull'. With gce instances, moving files through adb can be
painfully slow and hit timeouts, so the methods here just use scp instead.
"""
# pylint: disable=unused-argument
import logging
import os
import subprocess
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
from devil.utils import cmd_helper
logger = logging.getLogger(__name__)
class GceAdbWrapper(adb_wrapper.AdbWrapper):
def __init__(self, device_serial):
super(GceAdbWrapper, self).__init__(device_serial)
self._Connect()
self.Root()
self._instance_ip = self.Shell('getprop net.gce.ip').strip()
def _Connect(self, timeout=adb_wrapper.DEFAULT_TIMEOUT,
retries=adb_wrapper.DEFAULT_RETRIES):
"""Connects ADB to the android gce instance."""
cmd = ['connect', self._device_serial]
output = self._RunAdbCmd(cmd, timeout=timeout, retries=retries)
if 'unable to connect' in output:
raise device_errors.AdbCommandFailedError(cmd, output)
self.WaitForDevice()
# override
def Root(self, **kwargs):
super(GceAdbWrapper, self).Root()
self._Connect()
# override
def Push(self, local, remote, **kwargs):
"""Pushes an object from the host to the gce instance.
Args:
local: Path on the host filesystem.
remote: Path on the instance filesystem.
"""
adb_wrapper.VerifyLocalFileExists(local)
if os.path.isdir(local):
self.Shell('mkdir -p %s' % cmd_helper.SingleQuote(remote))
# When the object to be pushed is a directory, adb merges the source dir
# with the destination dir. So if local is a dir, just scp its contents.
for f in os.listdir(local):
self._PushObject(os.path.join(local, f), os.path.join(remote, f))
self.Shell('chmod 777 %s' %
cmd_helper.SingleQuote(os.path.join(remote, f)))
else:
parent_dir = remote[0:remote.rfind('/')]
if parent_dir:
self.Shell('mkdir -p %s' % cmd_helper.SingleQuote(parent_dir))
self._PushObject(local, remote)
self.Shell('chmod 777 %s' % cmd_helper.SingleQuote(remote))
def _PushObject(self, local, remote):
"""Copies an object from the host to the gce instance using scp.
Args:
local: Path on the host filesystem.
remote: Path on the instance filesystem.
"""
cmd = [
'scp',
'-r',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'StrictHostKeyChecking=no',
local,
'root@%s:%s' % (self._instance_ip, remote)
]
status, _ = cmd_helper.GetCmdStatusAndOutput(cmd)
if status:
raise device_errors.AdbCommandFailedError(
cmd, 'File not reachable on host: %s' % local,
device_serial=str(self))
# override
def Pull(self, remote, local, **kwargs):
"""Pulls a file from the gce instance to the host.
Args:
remote: Path on the instance filesystem.
local: Path on the host filesystem.
"""
cmd = [
'scp',
'-p',
'-r',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'StrictHostKeyChecking=no',
'root@%s:%s' % (self._instance_ip, remote),
local,
]
status, _ = cmd_helper.GetCmdStatusAndOutput(cmd)
if status:
raise device_errors.AdbCommandFailedError(
cmd, 'File not reachable on host: %s' % local,
device_serial=str(self))
try:
adb_wrapper.VerifyLocalFileExists(local)
except (subprocess.CalledProcessError, IOError):
logger.exception('Error when pulling files from android instance.')
raise device_errors.AdbCommandFailedError(
cmd, 'File not reachable on host: %s' % local,
device_serial=str(self))
# override
def Install(self, apk_path, forward_lock=False, reinstall=False,
sd_card=False, **kwargs):
"""Installs an apk on the gce instance
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.
"""
adb_wrapper.VerifyLocalFileExists(apk_path)
cmd = ['install']
if forward_lock:
cmd.append('-l')
if reinstall:
cmd.append('-r')
if sd_card:
cmd.append('-s')
self.Push(apk_path, '/data/local/tmp/tmp.apk')
cmd = ['pm'] + cmd
cmd.append('/data/local/tmp/tmp.apk')
output = self.Shell(' '.join(cmd))
self.Shell('rm /data/local/tmp/tmp.apk')
if 'Success' not in output:
raise device_errors.AdbCommandFailedError(
cmd, output, device_serial=self._device_serial)
# override
@property
def is_emulator(self):
return True

View File

@@ -0,0 +1,129 @@
# 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.
"""
# Some common flag constants that can be used to construct intents.
# Full list: http://developer.android.com/reference/android/content/Intent.html
FLAG_ACTIVITY_CLEAR_TASK = 0x00008000
FLAG_ACTIVITY_CLEAR_TOP = 0x04000000
FLAG_ACTIVITY_NEW_TASK = 0x10000000
FLAG_ACTIVITY_REORDER_TO_FRONT = 0x00020000
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 0x00200000
def _bitwise_or(flags):
result = 0
for flag in flags:
result |= flag
return result
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 sequence of flag constants to be passed with the intent.
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 = '0x%0.8x' % _bitwise_or(flags) if flags else None
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

View File

@@ -0,0 +1,63 @@
# 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.
"""Android KeyEvent constants.
http://developer.android.com/reference/android/view/KeyEvent.html
"""
KEYCODE_BACK = 4
KEYCODE_0 = 7
KEYCODE_1 = 8
KEYCODE_2 = 9
KEYCODE_3 = 10
KEYCODE_4 = 11
KEYCODE_5 = 12
KEYCODE_6 = 13
KEYCODE_7 = 14
KEYCODE_8 = 15
KEYCODE_9 = 16
KEYCODE_DPAD_RIGHT = 22
KEYCODE_POWER = 26
KEYCODE_A = 29
KEYCODE_B = 30
KEYCODE_C = 31
KEYCODE_D = 32
KEYCODE_E = 33
KEYCODE_F = 34
KEYCODE_G = 35
KEYCODE_H = 36
KEYCODE_I = 37
KEYCODE_J = 38
KEYCODE_K = 39
KEYCODE_L = 40
KEYCODE_M = 41
KEYCODE_N = 42
KEYCODE_O = 43
KEYCODE_P = 44
KEYCODE_Q = 45
KEYCODE_R = 46
KEYCODE_S = 47
KEYCODE_T = 48
KEYCODE_U = 49
KEYCODE_V = 50
KEYCODE_W = 51
KEYCODE_X = 52
KEYCODE_Y = 53
KEYCODE_Z = 54
KEYCODE_PERIOD = 56
KEYCODE_SPACE = 62
KEYCODE_ENTER = 66
KEYCODE_DEL = 67
KEYCODE_MENU = 82
KEYCODE_APP_SWITCH = 187

View File

@@ -0,0 +1,440 @@
# 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 logging
import posixpath
from xml.etree import ElementTree
from devil.android import device_errors
from devil.android.sdk import version_codes
logger = logging.getLogger(__name__)
_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, use_encrypted_path=False):
"""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.
use_encrypted_path: Whether to read and write to the shared prefs location
in the device-encrypted path (/data/user_de) instead of the older,
unencrypted path (/data/data). Only supported on N+, but falls back to
the unencrypted path if the encrypted path is not supported on the given
device.
"""
self._device = device
self._xml = None
self._package = package
self._filename = filename
self._unencrypted_path = '/data/data/%s/shared_prefs/%s' % (package,
filename)
self._encrypted_path = '/data/user_de/0/%s/shared_prefs/%s' % (package,
filename)
self._path = self._unencrypted_path
self._encrypted = use_encrypted_path
if use_encrypted_path:
if self._device.build_version_sdk < version_codes.NOUGAT:
logging.info('SharedPrefs set to use encrypted path, but given device '
'is not running N+. Falling back to unencrypted path')
self._encrypted = False
else:
self._path = self._encrypted_path
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, force_commit=False):
"""Save the current set of preferences to the device.
Only actually saves if some preferences have been modified or force_commit
is set to True.
Args:
force_commit: Commit even if no changes have been made to the SharedPrefs
instance.
"""
if not (self.changed or force_commit):
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)
# Creating the directory/file can cause issues with SELinux if they did
# not already exist. As a workaround, apply the package's security context
# to the shared_prefs directory, which mimics the behavior of a file
# created by the app itself
if self._device.build_version_sdk >= version_codes.MARSHMALLOW:
security_context = self._device.GetSecurityContextForPackage(self.package,
encrypted=self._encrypted)
if security_context is None:
raise device_errors.CommandFailedError(
'Failed to get security context for %s' % self.package)
paths = [posixpath.dirname(self.path), self.path]
self._device.ChangeSecurityContext(security_context, paths)
# Ensure that there isn't both an encrypted and unencrypted version of the
# file on the device at the same time.
if self._device.build_version_sdk >= version_codes.NOUGAT:
remove_path = (self._unencrypted_path if self._encrypted
else self._encrypted_path)
if self._device.PathExists(remove_path, as_root=True):
logging.warning('Found an equivalent shared prefs file at %s, removing',
remove_path)
self._device.RemovePath(remove_path, 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
logger.info('Setting property: %s', pref)

View File

@@ -0,0 +1,202 @@
#!/usr/bin/env python
# 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.
"""
Unit tests for the contents of shared_prefs.py (mostly SharedPrefs).
"""
import logging
import unittest
from devil import devil_env
from devil.android import device_utils
from devil.android.sdk import shared_prefs
from devil.android.sdk import version_codes
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
INITIAL_XML = ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
'<map>\n'
' <int name="databaseVersion" value="107" />\n'
' <boolean name="featureEnabled" value="false" />\n'
' <string name="someHashValue">249b3e5af13d4db2</string>\n'
'</map>')
def MockDeviceWithFiles(files=None):
if files is None:
files = {}
def file_exists(path):
return path in files
def write_file(path, contents, **_kwargs):
files[path] = contents
def read_file(path, **_kwargs):
return files[path]
device = mock.MagicMock(spec=device_utils.DeviceUtils)
device.FileExists = mock.Mock(side_effect=file_exists)
device.WriteFile = mock.Mock(side_effect=write_file)
device.ReadFile = mock.Mock(side_effect=read_file)
return device
class SharedPrefsTest(unittest.TestCase):
def setUp(self):
self.device = MockDeviceWithFiles({
'/data/data/com.some.package/shared_prefs/prefs.xml': INITIAL_XML})
self.expected_data = {'databaseVersion': 107,
'featureEnabled': False,
'someHashValue': '249b3e5af13d4db2'}
def testPropertyLifetime(self):
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml')
self.assertEquals(len(prefs), 0) # collection is empty before loading
prefs.SetInt('myValue', 444)
self.assertEquals(len(prefs), 1)
self.assertEquals(prefs.GetInt('myValue'), 444)
self.assertTrue(prefs.HasProperty('myValue'))
prefs.Remove('myValue')
self.assertEquals(len(prefs), 0)
self.assertFalse(prefs.HasProperty('myValue'))
with self.assertRaises(KeyError):
prefs.GetInt('myValue')
def testPropertyType(self):
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml')
prefs.SetInt('myValue', 444)
self.assertEquals(prefs.PropertyType('myValue'), 'int')
with self.assertRaises(TypeError):
prefs.GetString('myValue')
with self.assertRaises(TypeError):
prefs.SetString('myValue', 'hello')
def testLoad(self):
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml')
self.assertEquals(len(prefs), 0) # collection is empty before loading
prefs.Load()
self.assertEquals(len(prefs), len(self.expected_data))
self.assertEquals(prefs.AsDict(), self.expected_data)
self.assertFalse(prefs.changed)
def testClear(self):
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml')
prefs.Load()
self.assertEquals(prefs.AsDict(), self.expected_data)
self.assertFalse(prefs.changed)
prefs.Clear()
self.assertEquals(len(prefs), 0) # collection is empty now
self.assertTrue(prefs.changed)
def testCommit(self):
type(self.device).build_version_sdk = mock.PropertyMock(
return_value=version_codes.LOLLIPOP_MR1)
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'other_prefs.xml')
self.assertFalse(self.device.FileExists(prefs.path)) # file does not exist
prefs.Load()
self.assertEquals(len(prefs), 0) # file did not exist, collection is empty
prefs.SetInt('magicNumber', 42)
prefs.SetFloat('myMetric', 3.14)
prefs.SetLong('bigNumner', 6000000000)
prefs.SetStringSet('apps', ['gmail', 'chrome', 'music'])
self.assertFalse(self.device.FileExists(prefs.path)) # still does not exist
self.assertTrue(prefs.changed)
prefs.Commit()
self.assertTrue(self.device.FileExists(prefs.path)) # should exist now
self.device.KillAll.assert_called_once_with(prefs.package, exact=True,
as_root=True, quiet=True)
self.assertFalse(prefs.changed)
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'other_prefs.xml')
self.assertEquals(len(prefs), 0) # collection is empty before loading
prefs.Load()
self.assertEquals(prefs.AsDict(), {
'magicNumber': 42,
'myMetric': 3.14,
'bigNumner': 6000000000,
'apps': ['gmail', 'chrome', 'music']}) # data survived roundtrip
def testForceCommit(self):
prefs = shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml')
prefs.Load()
new_xml = 'Not valid XML'
self.device.WriteFile('/data/data/com.some.package/shared_prefs/prefs.xml',
new_xml)
prefs.Commit()
# Since we didn't change anything, Commit() should be a no-op.
self.assertEquals(self.device.ReadFile(
'/data/data/com.some.package/shared_prefs/prefs.xml'), new_xml)
prefs.Commit(force_commit=True)
# Forcing the commit should restore the originally read XML.
self.assertEquals(self.device.ReadFile(
'/data/data/com.some.package/shared_prefs/prefs.xml'), INITIAL_XML)
def testAsContextManager_onlyReads(self):
with shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml') as prefs:
self.assertEquals(prefs.AsDict(), self.expected_data) # loaded and ready
self.assertEquals(self.device.WriteFile.call_args_list, []) # did not write
def testAsContextManager_readAndWrite(self):
type(self.device).build_version_sdk = mock.PropertyMock(
return_value=version_codes.LOLLIPOP_MR1)
with shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml') as prefs:
prefs.SetBoolean('featureEnabled', True)
prefs.Remove('someHashValue')
prefs.SetString('newString', 'hello')
self.assertTrue(self.device.WriteFile.called) # did write
with shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml') as prefs:
# changes persisted
self.assertTrue(prefs.GetBoolean('featureEnabled'))
self.assertFalse(prefs.HasProperty('someHashValue'))
self.assertEquals(prefs.GetString('newString'), 'hello')
self.assertTrue(prefs.HasProperty('databaseVersion')) # still there
def testAsContextManager_commitAborted(self):
with self.assertRaises(TypeError):
with shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml') as prefs:
prefs.SetBoolean('featureEnabled', True)
prefs.Remove('someHashValue')
prefs.SetString('newString', 'hello')
prefs.SetInt('newString', 123) # oops!
self.assertEquals(self.device.WriteFile.call_args_list, []) # did not write
with shared_prefs.SharedPrefs(
self.device, 'com.some.package', 'prefs.xml') as prefs:
# contents were not modified
self.assertEquals(prefs.AsDict(), self.expected_data)
def testEncryptedPath(self):
type(self.device).build_version_sdk = mock.PropertyMock(
return_value=version_codes.MARSHMALLOW)
with shared_prefs.SharedPrefs(self.device, 'com.some.package',
'prefs.xml', use_encrypted_path=True) as prefs:
self.assertTrue(prefs.path.startswith('/data/data'))
type(self.device).build_version_sdk = mock.PropertyMock(
return_value=version_codes.NOUGAT)
with shared_prefs.SharedPrefs(self.device, 'com.some.package',
'prefs.xml', use_encrypted_path=True) as prefs:
self.assertTrue(prefs.path.startswith('/data/user_de/0'))
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)

View File

@@ -0,0 +1,63 @@
# 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."""
from devil.android.sdk import build_tools
from devil.utils import cmd_helper
from devil.utils import lazy
_split_select_path = lazy.WeakConstant(
lambda: build_tools.GetPath('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.read()] + 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, allow_cached_props=False):
"""Returns a config specifying which APK splits are required by the device.
Args:
device: A DeviceUtils object.
allow_cached_props: Whether to use cached values for device properties.
"""
return ('%s-r%s-%s:%s' %
(device.GetLanguage(cache=allow_cached_props),
device.GetCountry(cache=allow_cached_props),
device.screen_density,
device.product_cpu_abi))
def SelectSplits(device, base_apk, split_apks, allow_cached_props=False):
"""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.
allow_cached_props: Whether to use cached values for device properties.
Returns:
The list of APK splits that the device requires.
"""
config = _SplitConfig(device, allow_cached_props=allow_cached_props)
args = ['--target', config, '--base', base_apk]
for split in split_apks:
args.extend(['--split', split])
return _RunSplitSelectCmd(args).splitlines()

View File

@@ -0,0 +1 @@
Hello, world!

View File

@@ -0,0 +1,22 @@
# 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.
"""Android SDK version codes.
http://developer.android.com/reference/android/os/Build.VERSION_CODES.html
"""
JELLY_BEAN = 16
JELLY_BEAN_MR1 = 17
JELLY_BEAN_MR2 = 18
KITKAT = 19
KITKAT_WATCH = 20
LOLLIPOP = 21
LOLLIPOP_MR1 = 22
MARSHMALLOW = 23
NOUGAT = 24
NOUGAT_MR1 = 25
OREO = 26
OREO_MR1 = 27
PIE = 28

View File

@@ -0,0 +1,287 @@
# 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 logging
logger = logging.getLogger(__name__)
_LOCK_SCREEN_SETTINGS_PATH = '/data/system/locksettings.db'
_ALTERNATE_LOCK_SCREEN_SETTINGS_PATH = (
'/data/data/com.android.providers.settings/databases/settings.db')
PASSWORD_QUALITY_UNSPECIFIED = '0'
_COMPATIBLE_BUILD_TYPES = ['userdebug', 'eng']
ENABLE_LOCATION_SETTINGS = [
# Note that setting these in this order is required in order for all of
# them to take and stick through a reboot.
('com.google.settings/partner', [
('use_location_for_services', 1),
]),
('settings/secure', [
# Ensure Geolocation is enabled and allowed for tests.
('location_providers_allowed', 'gps,network'),
]),
('com.google.settings/partner', [
('network_location_opt_in', 1),
])
]
DISABLE_LOCATION_SETTINGS = [
('com.google.settings/partner', [
('use_location_for_services', 0),
]),
('settings/secure', [
# Ensure Geolocation is disabled.
('location_providers_allowed', ''),
]),
]
ENABLE_MOCK_LOCATION_SETTINGS = [
('settings/secure', [
('mock_location', 1),
]),
]
DISABLE_MOCK_LOCATION_SETTINGS = [
('settings/secure', [
('mock_location', 0),
]),
]
DETERMINISTIC_DEVICE_SETTINGS = [
('settings/global', [
('assisted_gps_enabled', 0),
# Disable "auto time" and "auto time zone" to avoid network-provided time
# to overwrite the device's datetime and timezone synchronized from host
# when running tests later. See b/6569849.
('auto_time', 0),
('auto_time_zone', 0),
('development_settings_enabled', 1),
# Flag for allowing ActivityManagerService to send ACTION_APP_ERROR intents
# on application crashes and ANRs. If this is disabled, the crash/ANR dialog
# will never display the "Report" button.
# Type: int ( 0 = disallow, 1 = allow )
('send_action_app_error', 0),
('stay_on_while_plugged_in', 3),
('verifier_verify_adb_installs', 0),
('window_animation_scale', 0),
]),
('settings/secure', [
('allowed_geolocation_origins',
'http://www.google.co.uk http://www.google.com'),
# Ensure that we never get random dialogs like "Unfortunately the process
# android.process.acore has stopped", which steal the focus, and make our
# automation fail (because the dialog steals the focus then mistakenly
# receives the injected user input events).
('anr_show_background', 0),
('lockscreen.disabled', 1),
('screensaver_enabled', 0),
('skip_first_use_hints', 1),
]),
('settings/system', [
# Don't want devices to accidentally rotate the screen as that could
# affect performance measurements.
('accelerometer_rotation', 0),
('lockscreen.disabled', 1),
# Turn down brightness and disable auto-adjust so that devices run cooler.
('screen_brightness', 5),
('screen_brightness_mode', 0),
('user_rotation', 0),
('window_animation_scale', 0),
]),
]
NETWORK_DISABLED_SETTINGS = [
('settings/global', [
('airplane_mode_on', 1),
('wifi_on', 0),
]),
]
class ContentSettings(dict):
"""A dict interface to interact with device content settings.
System properties are key/value pairs as exposed by adb shell content.
"""
def __init__(self, table, device):
super(ContentSettings, self).__init__()
self._table = table
self._device = device
@staticmethod
def _GetTypeBinding(value):
if isinstance(value, bool):
return 'b'
if isinstance(value, float):
return 'f'
if isinstance(value, int):
return 'i'
if isinstance(value, long):
return 'l'
if isinstance(value, str):
return 's'
raise ValueError('Unsupported type %s' % type(value))
def iteritems(self):
for row in self._device.RunShellCommand(
['content', 'query', '--uri', 'content://%s' % self._table],
check_return=True, as_root=True):
key, value = _ParseContentRow(row)
if not key:
continue
yield key, value
def __getitem__(self, key):
query_row = self._device.RunShellCommand(
['content', 'query', '--uri', 'content://%s' % self._table,
'--where', "name='%s'" % key],
check_return=True, as_root=True, single_line=True)
parsed_key, parsed_value = _ParseContentRow(query_row)
if parsed_key is None:
raise KeyError('key=%s not found' % key)
if parsed_key != key:
raise KeyError('Expected key=%s, but got key=%s' % (key, parsed_key))
return parsed_value
def __setitem__(self, key, value):
if key in self:
self._device.RunShellCommand(
['content', 'update', '--uri', 'content://%s' % self._table,
'--bind', 'value:%s:%s' % (self._GetTypeBinding(value), value),
'--where', "name='%s'" % key],
check_return=True, as_root=True)
else:
self._device.RunShellCommand(
['content', 'insert', '--uri', 'content://%s' % self._table,
'--bind', 'name:%s:%s' % (self._GetTypeBinding(key), key),
'--bind', 'value:%s:%s' % (self._GetTypeBinding(value), value)],
check_return=True, as_root=True)
def __delitem__(self, key):
self._device.RunShellCommand(
['content', 'delete', '--uri', 'content://%s' % self._table,
'--bind', 'name:%s:%s' % (self._GetTypeBinding(key), key)],
check_return=True, as_root=True)
def ConfigureContentSettings(device, desired_settings):
"""Configures device content setings from a list.
Many settings are documented at:
http://developer.android.com/reference/android/provider/Settings.Global.html
http://developer.android.com/reference/android/provider/Settings.Secure.html
http://developer.android.com/reference/android/provider/Settings.System.html
Many others are undocumented.
Args:
device: A DeviceUtils instance for the device to configure.
desired_settings: A list of (table, [(key: value), ...]) for all
settings to configure.
"""
for table, key_value in desired_settings:
settings = ContentSettings(table, device)
for key, value in key_value:
settings[key] = value
logger.info('\n%s %s', table, (80 - len(table)) * '-')
for key, value in sorted(settings.iteritems()):
logger.info('\t%s: %s', key, value)
def SetLockScreenSettings(device):
"""Sets lock screen settings on the device.
On certain device/Android configurations we need to disable the lock screen in
a different database. Additionally, the password type must be set to
DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED.
Lock screen settings are stored in sqlite on the device in:
/data/system/locksettings.db
IMPORTANT: The first column is used as a primary key so that all rows with the
same value for that column are removed from the table prior to inserting the
new values.
Args:
device: A DeviceUtils instance for the device to configure.
Raises:
Exception if the setting was not properly set.
"""
if device.build_type not in _COMPATIBLE_BUILD_TYPES:
logger.warning('Unable to disable lockscreen on %s builds.',
device.build_type)
return
def get_lock_settings(table):
return [(table, 'lockscreen.disabled', '1'),
(table, 'lockscreen.password_type', PASSWORD_QUALITY_UNSPECIFIED),
(table, 'lockscreen.password_type_alternate',
PASSWORD_QUALITY_UNSPECIFIED)]
if device.FileExists(_LOCK_SCREEN_SETTINGS_PATH):
db = _LOCK_SCREEN_SETTINGS_PATH
locksettings = get_lock_settings('locksettings')
columns = ['name', 'user', 'value']
generate_values = lambda k, v: [k, '0', v]
elif device.FileExists(_ALTERNATE_LOCK_SCREEN_SETTINGS_PATH):
db = _ALTERNATE_LOCK_SCREEN_SETTINGS_PATH
locksettings = get_lock_settings('secure') + get_lock_settings('system')
columns = ['name', 'value']
generate_values = lambda k, v: [k, v]
else:
logger.warning('Unable to find database file to set lock screen settings.')
return
for table, key, value in locksettings:
# Set the lockscreen setting for default user '0'
values = generate_values(key, value)
cmd = """begin transaction;
delete from '%(table)s' where %(primary_key)s='%(primary_value)s';
insert into '%(table)s' (%(columns)s) values (%(values)s);
commit transaction;""" % {
'table': table,
'primary_key': columns[0],
'primary_value': values[0],
'columns': ', '.join(columns),
'values': ', '.join(["'%s'" % value for value in values])
}
output_msg = device.RunShellCommand(
['sqlite3', db, cmd], check_return=True, as_root=True)
if output_msg:
logger.info(' '.join(output_msg))
def _ParseContentRow(row):
"""Parse key, value entries from a row string."""
# Example row:
# 'Row: 0 _id=13, name=logging_id2, value=-1fccbaa546705b05'
fields = row.split(', ')
key = None
value = ''
for field in fields:
k, _, v = field.partition('=')
if k == 'name':
key = v
elif k == 'value':
value = v
return key, value

View File

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

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python
# 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 argparse
import json
import os
import sys
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import device_utils
from devil.android.tools import script_common
from devil.utils import logging_common
def main():
parser = argparse.ArgumentParser(
'Run an adb shell command on selected devices')
parser.add_argument('cmd', help='Adb shell command to run.', nargs="+")
logging_common.AddLoggingArguments(parser)
script_common.AddDeviceArguments(parser)
script_common.AddEnvironmentArguments(parser)
parser.add_argument('--as-root', action='store_true', help='Run as root.')
parser.add_argument('--json-output',
help='File to dump json output to.')
args = parser.parse_args()
logging_common.InitializeLogging(args)
script_common.InitializeEnvironment(args)
devices = script_common.GetDevices(args.devices, args.blacklist_file)
p_out = (device_utils.DeviceUtils.parallel(devices).RunShellCommand(
args.cmd, large_output=True, as_root=args.as_root, check_return=True)
.pGet(None))
data = {}
for device, output in zip(devices, p_out):
for line in output:
print '%s: %s' % (device, line)
data[str(device)] = output
if args.json_output:
with open(args.json_output, 'w') as f:
json.dump(data, f)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,77 @@
#! /usr/bin/env python
# Copyright 2016 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 script to manipulate device CPU frequency."""
import argparse
import os
import pprint
import sys
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import device_utils
from devil.android.perf import perf_control
from devil.android.tools import script_common
from devil.utils import logging_common
def SetScalingGovernor(device, args):
p = perf_control.PerfControl(device)
p.SetScalingGovernor(args.governor)
def GetScalingGovernor(device, _args):
p = perf_control.PerfControl(device)
for cpu, governor in p.GetScalingGovernor():
print '%s %s: %s' % (str(device), cpu, governor)
def ListAvailableGovernors(device, _args):
p = perf_control.PerfControl(device)
for cpu, governors in p.ListAvailableGovernors():
print '%s %s: %s' % (str(device), cpu, pprint.pformat(governors))
def main(raw_args):
parser = argparse.ArgumentParser()
logging_common.AddLoggingArguments(parser)
script_common.AddEnvironmentArguments(parser)
parser.add_argument(
'--device', dest='devices', action='append', default=[],
help='Devices for which the governor should be set. Defaults to all.')
subparsers = parser.add_subparsers()
set_governor = subparsers.add_parser('set-governor')
set_governor.add_argument(
'governor',
help='Desired CPU governor.')
set_governor.set_defaults(func=SetScalingGovernor)
get_governor = subparsers.add_parser('get-governor')
get_governor.set_defaults(func=GetScalingGovernor)
list_governors = subparsers.add_parser('list-governors')
list_governors.set_defaults(func=ListAvailableGovernors)
args = parser.parse_args(raw_args)
logging_common.InitializeLogging(args)
script_common.InitializeEnvironment(args)
devices = device_utils.DeviceUtils.HealthyDevices(device_arg=args.devices)
parallel_devices = device_utils.DeviceUtils.parallel(devices)
parallel_devices.pMap(args.func, args)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,231 @@
#!/usr/bin/env python
# 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.
"""Launches a daemon to monitor android device temperatures & status.
This script will repeatedly poll the given devices for their temperatures and
status every 60 seconds and dump the stats to file on the host.
"""
import argparse
import collections
import json
import logging
import logging.handlers
import os
import re
import socket
import sys
import time
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import battery_utils
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_utils
from devil.android.tools import script_common
# Various names of sensors used to measure cpu temp
CPU_TEMP_SENSORS = [
# most nexus devices
'tsens_tz_sensor0',
# android one
'mtktscpu',
# nexus 9
'CPU-therm',
]
DEVICE_FILE_VERSION = 1
DEVICE_FILE = os.path.join(
os.path.expanduser('~'), '.android',
'%s__android_device_status.json' % socket.gethostname().split('.')[0])
MEM_INFO_REGEX = re.compile(r'.*?\:\s*(\d+)\s*kB') # ex: 'MemTotal: 185735 kB'
def get_device_status_unsafe(device):
"""Polls the given device for various info.
Returns: A dict of the following format:
{
'battery': {
'level': 100,
'temperature': 123
},
'build': {
'build.id': 'ABC12D',
'product.device': 'chickenofthesea'
},
'imei': 123456789,
'mem': {
'avail': 1000000,
'total': 1234567,
},
'processes': 123,
'state': 'good',
'temp': {
'some_sensor': 30
},
'uptime': 1234.56,
}
"""
status = collections.defaultdict(dict)
# Battery
battery = battery_utils.BatteryUtils(device)
battery_info = battery.GetBatteryInfo()
try:
level = int(battery_info.get('level'))
except (KeyError, TypeError, ValueError):
level = None
if level and level >= 0 and level <= 100:
status['battery']['level'] = level
try:
temperature = int(battery_info.get('temperature'))
except (KeyError, TypeError, ValueError):
temperature = None
if temperature:
status['battery']['temperature'] = temperature
# Build
status['build']['build.id'] = device.build_id
status['build']['product.device'] = device.build_product
# Memory
mem_info = ''
try:
mem_info = device.ReadFile('/proc/meminfo')
except device_errors.AdbShellCommandFailedError:
logging.exception('Unable to read /proc/meminfo')
for line in mem_info.splitlines():
match = MEM_INFO_REGEX.match(line)
if match:
try:
value = int(match.group(1))
except ValueError:
continue
key = line.split(':')[0].strip()
if key == 'MemTotal':
status['mem']['total'] = value
elif key == 'MemFree':
status['mem']['free'] = value
# Process
try:
status['processes'] = len(device.ListProcesses())
except device_errors.AdbCommandFailedError:
logging.exception('Unable to count process list.')
# CPU Temps
# Find a thermal sensor that matches one in CPU_TEMP_SENSORS and read its
# temperature.
files = []
try:
files = device.RunShellCommand(
'grep -lE "%s" /sys/class/thermal/thermal_zone*/type' % '|'.join(
CPU_TEMP_SENSORS), shell=True, check_return=True)
except device_errors.AdbShellCommandFailedError:
logging.exception('Unable to list thermal sensors.')
for f in files:
try:
sensor_name = device.ReadFile(f).strip()
temp = float(device.ReadFile(f[:-4] + 'temp').strip()) # s/type^/temp
status['temp'][sensor_name] = temp
except (device_errors.AdbShellCommandFailedError, ValueError):
logging.exception('Unable to read thermal sensor %s', f)
# Uptime
try:
uptimes = device.ReadFile('/proc/uptime').split()
status['uptime'] = float(uptimes[0]) # Take the first field (actual uptime)
except (device_errors.AdbShellCommandFailedError, ValueError):
logging.exception('Unable to read /proc/uptime')
try:
status['imei'] = device.GetIMEI()
except device_errors.CommandFailedError:
logging.exception('Unable to read IMEI')
status['imei'] = 'unknown'
status['state'] = 'available'
return status
def get_device_status(device):
try:
status = get_device_status_unsafe(device)
except device_errors.DeviceUnreachableError:
status = collections.defaultdict(dict)
status['state'] = 'offline'
return status
def get_all_status(blacklist):
status_dict = {
'version': DEVICE_FILE_VERSION,
'devices': {},
}
healthy_devices = device_utils.DeviceUtils.HealthyDevices(blacklist)
parallel_devices = device_utils.DeviceUtils.parallel(healthy_devices)
results = parallel_devices.pMap(get_device_status).pGet(None)
status_dict['devices'] = {
device.serial: result for device, result in zip(healthy_devices, results)
}
if blacklist:
for device, reason in blacklist.Read().iteritems():
status_dict['devices'][device] = {
'state': reason.get('reason', 'blacklisted')}
status_dict['timestamp'] = time.time()
return status_dict
def main(argv):
"""Launches the device monitor.
Polls the devices for their battery and cpu temperatures and scans the
blacklist file every 60 seconds and dumps the data to DEVICE_FILE.
"""
parser = argparse.ArgumentParser(
description='Launches the device monitor.')
script_common.AddEnvironmentArguments(parser)
parser.add_argument('--blacklist-file', help='Path to device blacklist file.')
args = parser.parse_args(argv)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler(
'/tmp/device_monitor.log', maxBytes=10 * 1024 * 1024, backupCount=5)
fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s',
datefmt='%y%m%d %H:%M:%S')
handler.setFormatter(fmt)
logger.addHandler(handler)
script_common.InitializeEnvironment(args)
blacklist = (device_blacklist.Blacklist(args.blacklist_file)
if args.blacklist_file else None)
logging.info('Device monitor running with pid %d, adb: %s, blacklist: %s',
os.getpid(), args.adb_path, args.blacklist_file)
while True:
start = time.time()
status_dict = get_all_status(blacklist)
with open(DEVICE_FILE, 'wb') as f:
json.dump(status_dict, f, indent=2, sort_keys=True)
logging.info('Got status of all devices in %.2fs.', time.time() - start)
time.sleep(60)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env python
# 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
import sys
import unittest
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil import devil_env
from devil.android import device_errors
from devil.android import device_utils
from devil.android.tools import device_monitor
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
class DeviceMonitorTest(unittest.TestCase):
def setUp(self):
self.device = mock.Mock(spec=device_utils.DeviceUtils,
serial='device_cereal', build_id='abc123', build_product='clownfish',
GetIMEI=lambda: '123456789')
self.file_contents = {
'/proc/meminfo': """
MemTotal: 1234567 kB
MemFree: 1000000 kB
MemUsed: 234567 kB
""",
'/sys/class/thermal/thermal_zone0/type': 'CPU-therm',
'/sys/class/thermal/thermal_zone0/temp': '30',
'/proc/uptime': '12345 99999',
}
self.device.ReadFile = mock.MagicMock(
side_effect=lambda file_name: self.file_contents[file_name])
self.device.ListProcesses.return_value = ['p1', 'p2', 'p3', 'p4', 'p5']
self.cmd_outputs = {
'grep': ['/sys/class/thermal/thermal_zone0/type'],
}
def mock_run_shell(cmd, **_kwargs):
args = cmd.split() if isinstance(cmd, basestring) else cmd
try:
return self.cmd_outputs[args[0]]
except KeyError:
raise device_errors.AdbShellCommandFailedError(cmd, None, None)
self.device.RunShellCommand = mock.MagicMock(side_effect=mock_run_shell)
self.battery = mock.Mock()
self.battery.GetBatteryInfo = mock.MagicMock(
return_value={'level': '80', 'temperature': '123'})
self.expected_status = {
'device_cereal': {
'processes': 5,
'temp': {
'CPU-therm': 30.0
},
'battery': {
'temperature': 123,
'level': 80
},
'uptime': 12345.0,
'mem': {
'total': 1234567,
'free': 1000000
},
'build': {
'build.id': 'abc123',
'product.device': 'clownfish',
},
'imei': '123456789',
'state': 'available',
}
}
@mock.patch('devil.android.battery_utils.BatteryUtils')
@mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices')
def test_getStats(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
status = device_monitor.get_all_status(None)
self.assertEquals(self.expected_status, status['devices'])
@mock.patch('devil.android.battery_utils.BatteryUtils')
@mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices')
def test_getStatsNoBattery(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
broken_battery_info = mock.Mock()
broken_battery_info.GetBatteryInfo = mock.MagicMock(
return_value={'level': '-1', 'temperature': 'not_a_number'})
get_battery.return_value = broken_battery_info
# Should be same status dict but without battery stats.
expected_status_no_battery = self.expected_status.copy()
expected_status_no_battery['device_cereal'].pop('battery')
status = device_monitor.get_all_status(None)
self.assertEquals(expected_status_no_battery, status['devices'])
@mock.patch('devil.android.battery_utils.BatteryUtils')
@mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices')
def test_getStatsNoPs(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
# Throw exception when listing processes.
self.device.ListProcesses.side_effect = device_errors.AdbCommandFailedError(
['ps'], 'something failed', 1)
# Should be same status dict but without process stats.
expected_status_no_ps = self.expected_status.copy()
expected_status_no_ps['device_cereal'].pop('processes')
status = device_monitor.get_all_status(None)
self.assertEquals(expected_status_no_ps, status['devices'])
@mock.patch('devil.android.battery_utils.BatteryUtils')
@mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices')
def test_getStatsNoSensors(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
del self.cmd_outputs['grep'] # Throw exception on run shell grep command.
# Should be same status dict but without temp stats.
expected_status_no_temp = self.expected_status.copy()
expected_status_no_temp['device_cereal'].pop('temp')
status = device_monitor.get_all_status(None)
self.assertEquals(expected_status_no_temp, status['devices'])
@mock.patch('devil.android.battery_utils.BatteryUtils')
@mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices')
def test_getStatsWithBlacklist(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
blacklist = mock.Mock()
blacklist.Read = mock.MagicMock(
return_value={'bad_device': {'reason': 'offline'}})
# Should be same status dict but with extra blacklisted device.
expected_status = self.expected_status.copy()
expected_status['bad_device'] = {'state': 'offline'}
status = device_monitor.get_all_status(blacklist)
self.assertEquals(expected_status, status['devices'])
@mock.patch('devil.android.battery_utils.BatteryUtils')
@mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices')
def test_brokenTempValue(self, get_devices, get_battery):
self.file_contents['/sys/class/thermal/thermal_zone0/temp'] = 'n0t a numb3r'
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
expected_status_no_temp = self.expected_status.copy()
expected_status_no_temp['device_cereal'].pop('temp')
status = device_monitor.get_all_status(None)
self.assertEquals(self.expected_status, status['devices'])
if __name__ == '__main__':
sys.exit(unittest.main())

View File

@@ -0,0 +1,262 @@
#!/usr/bin/env vpython
# Copyright 2016 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 script to recover devices in a known bad state."""
import argparse
import glob
import logging
import os
import signal
import sys
import psutil
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
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.tools import device_status
from devil.android.tools import script_common
from devil.utils import logging_common
from devil.utils import lsusb
# TODO(jbudorick): Resolve this after experimenting w/ disabling the USB reset.
from devil.utils import reset_usb # pylint: disable=unused-import
logger = logging.getLogger(__name__)
from py_utils import modules_util
# Script depends on features from psutil version 2.0 or higher.
modules_util.RequireVersion(psutil, '2.0')
def KillAllAdb():
def get_all_adb():
for p in psutil.process_iter():
try:
# Retrieve all required process infos at once.
pinfo = p.as_dict(attrs=['pid', 'name', 'cmdline'])
if pinfo['name'] == 'adb':
pinfo['cmdline'] = ' '.join(pinfo['cmdline'])
yield p, pinfo
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
for sig in [signal.SIGTERM, signal.SIGQUIT, signal.SIGKILL]:
for p, pinfo in get_all_adb():
try:
pinfo['signal'] = sig
logger.info('kill %(signal)s %(pid)s (%(name)s [%(cmdline)s])', pinfo)
p.send_signal(sig)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
for _, pinfo in get_all_adb():
try:
logger.error('Unable to kill %(pid)s (%(name)s [%(cmdline)s])', pinfo)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
def TryAuth(device):
"""Uses anything in ~/.android/ that looks like a key to auth with the device.
Args:
device: The DeviceUtils device to attempt to auth.
Returns:
True if device successfully authed.
"""
possible_keys = glob.glob(os.path.join(adb_wrapper.ADB_HOST_KEYS_DIR, '*key'))
if len(possible_keys) <= 1:
logger.warning(
'Only %d ADB keys available. Not forcing auth.', len(possible_keys))
return False
KillAllAdb()
adb_wrapper.AdbWrapper.StartServer(keys=possible_keys)
new_state = device.adb.GetState()
if new_state != 'device':
logger.error(
'Auth failed. Device %s still stuck in %s.', str(device), new_state)
return False
# It worked! Now register the host's default ADB key on the device so we don't
# have to do all that again.
pub_key = os.path.join(adb_wrapper.ADB_HOST_KEYS_DIR, 'adbkey.pub')
if not os.path.exists(pub_key): # This really shouldn't happen.
logger.error('Default ADB key not available at %s.', pub_key)
return False
with open(pub_key) as f:
pub_key_contents = f.read()
try:
device.WriteFile(adb_wrapper.ADB_KEYS_FILE, pub_key_contents, as_root=True)
except (device_errors.CommandTimeoutError,
device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Unable to write default ADB key to %s.', str(device))
return False
return True
def RecoverDevice(device, blacklist, should_reboot=lambda device: True):
if device_status.IsBlacklisted(device.adb.GetDeviceSerial(),
blacklist):
logger.debug('%s is blacklisted, skipping recovery.', str(device))
return
if device.adb.GetState() == 'unauthorized' and TryAuth(device):
logger.info('Successfully authed device %s!', str(device))
return
if should_reboot(device):
try:
device.WaitUntilFullyBooted(retries=0)
except (device_errors.CommandTimeoutError,
device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Failure while waiting for %s. '
'Attempting to recover.', str(device))
try:
try:
device.Reboot(block=False, timeout=5, retries=0)
except device_errors.CommandTimeoutError:
logger.warning('Timed out while attempting to reboot %s normally.'
'Attempting alternative reboot.', str(device))
# The device drops offline before we can grab the exit code, so
# we don't check for status.
try:
device.adb.Root()
finally:
# We are already in a failure mode, attempt to reboot regardless of
# what device.adb.Root() returns. If the sysrq reboot fails an
# exception willbe thrown at that level.
device.adb.Shell('echo b > /proc/sysrq-trigger', expect_status=None,
timeout=5, retries=0)
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Failed to reboot %s.', str(device))
if blacklist:
blacklist.Extend([device.adb.GetDeviceSerial()],
reason='reboot_failure')
except device_errors.CommandTimeoutError:
logger.exception('Timed out while rebooting %s.', str(device))
if blacklist:
blacklist.Extend([device.adb.GetDeviceSerial()],
reason='reboot_timeout')
try:
device.WaitUntilFullyBooted(
retries=0, timeout=device.REBOOT_DEFAULT_TIMEOUT)
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Failure while waiting for %s.', str(device))
if blacklist:
blacklist.Extend([device.adb.GetDeviceSerial()],
reason='reboot_failure')
except device_errors.CommandTimeoutError:
logger.exception('Timed out while waiting for %s.', str(device))
if blacklist:
blacklist.Extend([device.adb.GetDeviceSerial()],
reason='reboot_timeout')
def RecoverDevices(devices, blacklist, enable_usb_reset=False):
"""Attempts to recover any inoperable devices in the provided list.
Args:
devices: The list of devices to attempt to recover.
blacklist: The current device blacklist, which will be used then
reset.
"""
statuses = device_status.DeviceStatus(devices, blacklist)
should_restart_usb = set(
status['serial'] for status in statuses
if (not status['usb_status']
or status['adb_status'] in ('offline', 'missing')))
should_restart_adb = should_restart_usb.union(set(
status['serial'] for status in statuses
if status['adb_status'] == 'unauthorized'))
should_reboot_device = should_restart_usb.union(set(
status['serial'] for status in statuses
if status['blacklisted']))
logger.debug('Should restart USB for:')
for d in should_restart_usb:
logger.debug(' %s', d)
logger.debug('Should restart ADB for:')
for d in should_restart_adb:
logger.debug(' %s', d)
logger.debug('Should reboot:')
for d in should_reboot_device:
logger.debug(' %s', d)
if blacklist:
blacklist.Reset()
if should_restart_adb:
KillAllAdb()
adb_wrapper.AdbWrapper.StartServer()
for serial in should_restart_usb:
try:
# TODO(crbug.com/642194): Resetting may be causing more harm
# (specifically, kernel panics) than it does good.
if enable_usb_reset:
reset_usb.reset_android_usb(serial)
else:
logger.warning('USB reset disabled for %s (crbug.com/642914)',
serial)
except IOError:
logger.exception('Unable to reset USB for %s.', serial)
if blacklist:
blacklist.Extend([serial], reason='USB failure')
except device_errors.DeviceUnreachableError:
logger.exception('Unable to reset USB for %s.', serial)
if blacklist:
blacklist.Extend([serial], reason='offline')
device_utils.DeviceUtils.parallel(devices).pMap(
RecoverDevice, blacklist,
should_reboot=lambda device: device.serial in should_reboot_device)
def main():
parser = argparse.ArgumentParser()
logging_common.AddLoggingArguments(parser)
script_common.AddEnvironmentArguments(parser)
parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
parser.add_argument('--known-devices-file', action='append', default=[],
dest='known_devices_files',
help='Path to known device lists.')
parser.add_argument('--enable-usb-reset', action='store_true',
help='Reset USB if necessary.')
args = parser.parse_args()
logging_common.InitializeLogging(args)
script_common.InitializeEnvironment(args)
blacklist = (device_blacklist.Blacklist(args.blacklist_file)
if args.blacklist_file
else None)
expected_devices = device_status.GetExpectedDevices(args.known_devices_files)
usb_devices = set(lsusb.get_android_devices())
devices = [device_utils.DeviceUtils(s)
for s in expected_devices.union(usb_devices)]
RecoverDevices(devices, blacklist, enable_usb_reset=args.enable_usb_reset)
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,296 @@
#!/usr/bin/env python
# Copyright 2016 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 script to keep track of devices across builds and report state."""
import argparse
import json
import logging
import os
import re
import sys
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import battery_utils
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_list
from devil.android import device_utils
from devil.android.sdk import adb_wrapper
from devil.android.tools import script_common
from devil.constants import exit_codes
from devil.utils import logging_common
from devil.utils import lsusb
logger = logging.getLogger(__name__)
_RE_DEVICE_ID = re.compile(r'Device ID = (\d+)')
def IsBlacklisted(serial, blacklist):
return blacklist and serial in blacklist.Read()
def _BatteryStatus(device, blacklist):
battery_info = {}
try:
battery = battery_utils.BatteryUtils(device)
battery_info = battery.GetBatteryInfo(timeout=5)
battery_level = int(battery_info.get('level', 100))
if battery_level < 15:
logger.error('Critically low battery level (%d)', battery_level)
battery = battery_utils.BatteryUtils(device)
if not battery.GetCharging():
battery.SetCharging(True)
if blacklist:
blacklist.Extend([device.adb.GetDeviceSerial()], reason='low_battery')
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Failed to get battery information for %s',
str(device))
return battery_info
def DeviceStatus(devices, blacklist):
"""Generates status information for the given devices.
Args:
devices: The devices to generate status for.
blacklist: The current device blacklist.
Returns:
A dict of the following form:
{
'<serial>': {
'serial': '<serial>',
'adb_status': str,
'usb_status': bool,
'blacklisted': bool,
# only if the device is connected and not blacklisted
'type': ro.build.product,
'build': ro.build.id,
'build_detail': ro.build.fingerprint,
'battery': {
...
},
'imei_slice': str,
'wifi_ip': str,
},
...
}
"""
adb_devices = {
a[0].GetDeviceSerial(): a
for a in adb_wrapper.AdbWrapper.Devices(desired_state=None, long_list=True)
}
usb_devices = set(lsusb.get_android_devices())
def blacklisting_device_status(device):
serial = device.adb.GetDeviceSerial()
adb_status = (
adb_devices[serial][1] if serial in adb_devices
else 'missing')
usb_status = bool(serial in usb_devices)
device_status = {
'serial': serial,
'adb_status': adb_status,
'usb_status': usb_status,
}
if not IsBlacklisted(serial, blacklist):
if adb_status == 'device':
try:
build_product = device.build_product
build_id = device.build_id
build_fingerprint = device.build_fingerprint
build_description = device.build_description
wifi_ip = device.GetProp('dhcp.wlan0.ipaddress')
battery_info = _BatteryStatus(device, blacklist)
try:
imei_slice = device.GetIMEI()
except device_errors.CommandFailedError:
logging.exception('Unable to fetch IMEI for %s.', str(device))
imei_slice = 'unknown'
if (device.product_name == 'mantaray' and
battery_info.get('AC powered', None) != 'true'):
logger.error('Mantaray device not connected to AC power.')
device_status.update({
'ro.build.product': build_product,
'ro.build.id': build_id,
'ro.build.fingerprint': build_fingerprint,
'ro.build.description': build_description,
'battery': battery_info,
'imei_slice': imei_slice,
'wifi_ip': wifi_ip,
})
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Failure while getting device status for %s.',
str(device))
if blacklist:
blacklist.Extend([serial], reason='status_check_failure')
except device_errors.CommandTimeoutError:
logger.exception('Timeout while getting device status for %s.',
str(device))
if blacklist:
blacklist.Extend([serial], reason='status_check_timeout')
elif blacklist:
blacklist.Extend([serial],
reason=adb_status if usb_status else 'offline')
device_status['blacklisted'] = IsBlacklisted(serial, blacklist)
return device_status
parallel_devices = device_utils.DeviceUtils.parallel(devices)
statuses = parallel_devices.pMap(blacklisting_device_status).pGet(None)
return statuses
def _LogStatuses(statuses):
# Log the state of all devices.
for status in statuses:
logger.info(status['serial'])
adb_status = status.get('adb_status')
blacklisted = status.get('blacklisted')
logger.info(' USB status: %s',
'online' if status.get('usb_status') else 'offline')
logger.info(' ADB status: %s', adb_status)
logger.info(' Blacklisted: %s', str(blacklisted))
if adb_status == 'device' and not blacklisted:
logger.info(' Device type: %s', status.get('ro.build.product'))
logger.info(' OS build: %s', status.get('ro.build.id'))
logger.info(' OS build fingerprint: %s',
status.get('ro.build.fingerprint'))
logger.info(' Battery state:')
for k, v in status.get('battery', {}).iteritems():
logger.info(' %s: %s', k, v)
logger.info(' IMEI slice: %s', status.get('imei_slice'))
logger.info(' WiFi IP: %s', status.get('wifi_ip'))
def _WriteBuildbotFile(file_path, statuses):
buildbot_path, _ = os.path.split(file_path)
if os.path.exists(buildbot_path):
with open(file_path, 'w') as f:
for status in statuses:
try:
if status['adb_status'] == 'device':
f.write('{serial} {adb_status} {build_product} {build_id} '
'{temperature:.1f}C {level}%\n'.format(
serial=status['serial'],
adb_status=status['adb_status'],
build_product=status['type'],
build_id=status['build'],
temperature=float(status['battery']['temperature']) / 10,
level=status['battery']['level']
))
elif status.get('usb_status', False):
f.write('{serial} {adb_status}\n'.format(
serial=status['serial'],
adb_status=status['adb_status']
))
else:
f.write('{serial} offline\n'.format(
serial=status['serial']
))
except Exception: # pylint: disable=broad-except
pass
def GetExpectedDevices(known_devices_files):
expected_devices = set()
try:
for path in known_devices_files:
if os.path.exists(path):
expected_devices.update(device_list.GetPersistentDeviceList(path))
else:
logger.warning('Could not find known devices file: %s', path)
except IOError:
logger.warning('Problem reading %s, skipping.', path)
logger.info('Expected devices:')
for device in expected_devices:
logger.info(' %s', device)
return expected_devices
def AddArguments(parser):
parser.add_argument('--json-output',
help='Output JSON information into a specified file.')
parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
parser.add_argument('--known-devices-file', action='append', default=[],
dest='known_devices_files',
help='Path to known device lists.')
parser.add_argument('--buildbot-path', '-b',
default='/home/chrome-bot/.adb_device_info',
help='Absolute path to buildbot file location')
parser.add_argument('-w', '--overwrite-known-devices-files',
action='store_true',
help='If set, overwrites known devices files wiht new '
'values.')
def main():
parser = argparse.ArgumentParser()
logging_common.AddLoggingArguments(parser)
script_common.AddEnvironmentArguments(parser)
AddArguments(parser)
args = parser.parse_args()
logging_common.InitializeLogging(args)
script_common.InitializeEnvironment(args)
blacklist = (device_blacklist.Blacklist(args.blacklist_file)
if args.blacklist_file
else None)
expected_devices = GetExpectedDevices(args.known_devices_files)
usb_devices = set(lsusb.get_android_devices())
devices = [device_utils.DeviceUtils(s)
for s in expected_devices.union(usb_devices)]
statuses = DeviceStatus(devices, blacklist)
# Log the state of all devices.
_LogStatuses(statuses)
# Update the last devices file(s).
if args.overwrite_known_devices_files:
for path in args.known_devices_files:
device_list.WritePersistentDeviceList(
path, [status['serial'] for status in statuses])
# Write device info to file for buildbot info display.
_WriteBuildbotFile(args.buildbot_path, statuses)
# Dump the device statuses to JSON.
if args.json_output:
with open(args.json_output, 'wb') as f:
f.write(json.dumps(
statuses, indent=4, sort_keys=True, separators=(',', ': ')))
live_devices = [status['serial'] for status in statuses
if (status['adb_status'] == 'device'
and not IsBlacklisted(status['serial'], blacklist))]
# If all devices failed, or if there are no devices, it's an infra error.
if not live_devices:
logger.error('No available devices.')
return 0 if live_devices else exit_codes.INFRA
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python
# 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 argparse
import logging
import os
import sys
if __name__ == '__main__':
sys.path.append(os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..')))
from devil.android import device_blacklist
from devil.android import device_utils
from devil.android import fastboot_utils
from devil.android.tools import script_common
from devil.constants import exit_codes
from devil.utils import logging_common
logger = logging.getLogger(__name__)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('build_path', help='Path to android build.')
parser.add_argument('-w', '--wipe', action='store_true',
help='If set, wipes user data')
logging_common.AddLoggingArguments(parser)
script_common.AddDeviceArguments(parser)
args = parser.parse_args()
logging_common.InitializeLogging(args)
if args.blacklist_file:
blacklist = device_blacklist.Blacklist(args.blacklist_file).Read()
if blacklist:
logger.critical('Device(s) in blacklist, not flashing devices:')
for key in blacklist:
logger.critical(' %s', key)
return exit_codes.INFRA
flashed_devices = []
failed_devices = []
def flash(device):
fastboot = fastboot_utils.FastbootUtils(device)
try:
fastboot.FlashDevice(args.build_path, wipe=args.wipe)
flashed_devices.append(device)
except Exception: # pylint: disable=broad-except
logger.exception('Device %s failed to flash.', str(device))
failed_devices.append(device)
devices = script_common.GetDevices(args.devices, args.blacklist_file)
device_utils.DeviceUtils.parallel(devices).pMap(flash)
if flashed_devices:
logger.info('The following devices were flashed:')
logger.info(' %s', ' '.join(str(d) for d in flashed_devices))
if failed_devices:
logger.critical('The following devices failed to flash:')
logger.critical(' %s', ' '.join(str(d) for d in failed_devices))
return exit_codes.INFRA
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python
# Copyright 2016 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.
"""Use your keyboard as your phone's keyboard. Experimental."""
import argparse
import copy
import os
import sys
import termios
import tty
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil import base_error
from devil.android.sdk import keyevent
from devil.android.tools import script_common
from devil.utils import logging_common
_KEY_MAPPING = {
'\x08': keyevent.KEYCODE_DEL,
'\x0a': keyevent.KEYCODE_ENTER,
' ': keyevent.KEYCODE_SPACE,
'.': keyevent.KEYCODE_PERIOD,
'0': keyevent.KEYCODE_0,
'1': keyevent.KEYCODE_1,
'2': keyevent.KEYCODE_2,
'3': keyevent.KEYCODE_3,
'4': keyevent.KEYCODE_4,
'5': keyevent.KEYCODE_5,
'6': keyevent.KEYCODE_6,
'7': keyevent.KEYCODE_7,
'8': keyevent.KEYCODE_8,
'9': keyevent.KEYCODE_9,
'a': keyevent.KEYCODE_A,
'b': keyevent.KEYCODE_B,
'c': keyevent.KEYCODE_C,
'd': keyevent.KEYCODE_D,
'e': keyevent.KEYCODE_E,
'f': keyevent.KEYCODE_F,
'g': keyevent.KEYCODE_G,
'h': keyevent.KEYCODE_H,
'i': keyevent.KEYCODE_I,
'j': keyevent.KEYCODE_J,
'k': keyevent.KEYCODE_K,
'l': keyevent.KEYCODE_L,
'm': keyevent.KEYCODE_M,
'n': keyevent.KEYCODE_N,
'o': keyevent.KEYCODE_O,
'p': keyevent.KEYCODE_P,
'q': keyevent.KEYCODE_Q,
'r': keyevent.KEYCODE_R,
's': keyevent.KEYCODE_S,
't': keyevent.KEYCODE_T,
'u': keyevent.KEYCODE_U,
'v': keyevent.KEYCODE_V,
'w': keyevent.KEYCODE_W,
'x': keyevent.KEYCODE_X,
'y': keyevent.KEYCODE_Y,
'z': keyevent.KEYCODE_Z,
'\x7f': keyevent.KEYCODE_DEL,
}
def Keyboard(device, stream_itr):
try:
for c in stream_itr:
k = _KEY_MAPPING.get(c)
if k:
device.SendKeyEvent(k)
else:
print
print '(No mapping for character 0x%x)' % ord(c)
except KeyboardInterrupt:
pass
class MultipleDevicesError(base_error.BaseError):
def __init__(self, devices):
super(MultipleDevicesError, self).__init__(
'More than one device found: %s' % ', '.join(str(d) for d in devices))
def main(raw_args):
parser = argparse.ArgumentParser(
description="Use your keyboard as your phone's keyboard.")
logging_common.AddLoggingArguments(parser)
script_common.AddDeviceArguments(parser)
args = parser.parse_args(raw_args)
logging_common.InitializeLogging(args)
devices = script_common.GetDevices(args.devices, None)
if len(devices) > 1:
raise MultipleDevicesError(devices)
def next_char():
while True:
yield sys.stdin.read(1)
try:
fd = sys.stdin.fileno()
# See man 3 termios for more info on what this is doing.
old_attrs = termios.tcgetattr(fd)
new_attrs = copy.deepcopy(old_attrs)
new_attrs[tty.LFLAG] = new_attrs[tty.LFLAG] & ~(termios.ICANON)
new_attrs[tty.CC][tty.VMIN] = 1
new_attrs[tty.CC][tty.VTIME] = 0
termios.tcsetattr(fd, termios.TCSAFLUSH, new_attrs)
Keyboard(devices[0], next_char())
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, old_attrs)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,678 @@
#!/usr/bin/env python
#
# 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.
"""Provisions Android devices with settings required for bots.
Usage:
./provision_devices.py [-d <device serial number>]
"""
import argparse
import datetime
import json
import logging
import os
import posixpath
import re
import sys
import time
# Import _strptime before threaded code. datetime.datetime.strptime is
# threadsafe except for the initial import of the _strptime module.
# See crbug.com/584730 and https://bugs.python.org/issue7980.
import _strptime # pylint: disable=unused-import
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import battery_utils
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_temp_file
from devil.android import device_utils
from devil.android import settings
from devil.android.sdk import adb_wrapper
from devil.android.sdk import intent
from devil.android.sdk import keyevent
from devil.android.sdk import shared_prefs
from devil.android.sdk import version_codes
from devil.android.tools import script_common
from devil.constants import exit_codes
from devil.utils import logging_common
from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
_SYSTEM_APP_DIRECTORIES = ['/system/app/', '/system/priv-app/']
_SYSTEM_WEBVIEW_NAMES = ['webview', 'WebViewGoogle']
_CHROME_PACKAGE_REGEX = re.compile('.*chrom.*')
_TOMBSTONE_REGEX = re.compile('tombstone.*')
_STANDALONE_VR_DEVICES = [
'vega', # Lenovo Mirage Solo
]
class _DEFAULT_TIMEOUTS(object):
# L can take a while to reboot after a wipe.
LOLLIPOP = 600
PRE_LOLLIPOP = 180
HELP_TEXT = '{}s on L, {}s on pre-L'.format(LOLLIPOP, PRE_LOLLIPOP)
class ProvisionStep(object):
def __init__(self, cmd, reboot=False):
self.cmd = cmd
self.reboot = reboot
def ProvisionDevices(
devices,
blacklist_file,
adb_key_files=None,
disable_location=False,
disable_mock_location=False,
disable_network=False,
disable_system_chrome=False,
emulators=False,
enable_java_debug=False,
max_battery_temp=None,
min_battery_level=None,
output_device_blacklist=None,
reboot_timeout=None,
remove_system_webview=False,
system_app_remove_list=None,
system_package_remove_list=None,
wipe=True):
blacklist = (device_blacklist.Blacklist(blacklist_file)
if blacklist_file
else None)
system_app_remove_list = system_app_remove_list or []
system_package_remove_list = system_package_remove_list or []
try:
devices = script_common.GetDevices(devices, blacklist)
except device_errors.NoDevicesError:
logging.error('No available devices to provision.')
if blacklist:
logging.error('Local device blacklist: %s', blacklist.Read())
raise
devices = [d for d in devices
if not emulators or d.adb.is_emulator]
parallel_devices = device_utils.DeviceUtils.parallel(devices)
steps = []
if wipe:
steps += [ProvisionStep(lambda d: Wipe(d, adb_key_files), reboot=True)]
steps += [ProvisionStep(
lambda d: SetProperties(d, enable_java_debug, disable_location,
disable_mock_location),
reboot=not emulators)]
if disable_network:
steps.append(ProvisionStep(DisableNetwork))
if disable_system_chrome:
steps.append(ProvisionStep(DisableSystemChrome))
if max_battery_temp:
steps.append(ProvisionStep(
lambda d: WaitForBatteryTemperature(d, max_battery_temp)))
if min_battery_level:
steps.append(ProvisionStep(
lambda d: WaitForCharge(d, min_battery_level)))
if remove_system_webview:
system_app_remove_list.extend(_SYSTEM_WEBVIEW_NAMES)
if system_app_remove_list or system_package_remove_list:
steps.append(ProvisionStep(
lambda d: RemoveSystemApps(
d, system_app_remove_list, system_package_remove_list)))
steps.append(ProvisionStep(SetDate))
steps.append(ProvisionStep(CheckExternalStorage))
steps.append(ProvisionStep(StandaloneVrDeviceSetup))
parallel_devices.pMap(ProvisionDevice, steps, blacklist, reboot_timeout)
blacklisted_devices = blacklist.Read() if blacklist else []
if output_device_blacklist:
with open(output_device_blacklist, 'w') as f:
json.dump(blacklisted_devices, f)
if all(d in blacklisted_devices for d in devices):
raise device_errors.NoDevicesError
return 0
def ProvisionDevice(device, steps, blacklist, reboot_timeout=None):
try:
if not reboot_timeout:
if device.build_version_sdk >= version_codes.LOLLIPOP:
reboot_timeout = _DEFAULT_TIMEOUTS.LOLLIPOP
else:
reboot_timeout = _DEFAULT_TIMEOUTS.PRE_LOLLIPOP
for step in steps:
try:
device.WaitUntilFullyBooted(timeout=reboot_timeout, retries=0)
except device_errors.CommandTimeoutError:
logger.error('Device did not finish booting. Will try to reboot.')
device.Reboot(timeout=reboot_timeout)
step.cmd(device)
if step.reboot:
device.Reboot(False, retries=0)
device.adb.WaitForDevice()
except device_errors.CommandTimeoutError:
logger.exception('Timed out waiting for device %s. Adding to blacklist.',
str(device))
if blacklist:
blacklist.Extend([str(device)], reason='provision_timeout')
except (device_errors.CommandFailedError,
device_errors.DeviceUnreachableError):
logger.exception('Failed to provision device %s. Adding to blacklist.',
str(device))
if blacklist:
blacklist.Extend([str(device)], reason='provision_failure')
def Wipe(device, adb_key_files=None):
if (device.IsUserBuild() or
device.build_version_sdk >= version_codes.MARSHMALLOW):
WipeChromeData(device)
package = 'com.google.android.gms'
if device.GetApplicationPaths(package):
version_name = device.GetApplicationVersion(package)
logger.info('Version name for %s is %s', package, version_name)
else:
logger.info('Package %s is not installed', package)
else:
WipeDevice(device, adb_key_files)
def WipeChromeData(device):
"""Wipes chrome specific data from device
(1) uninstall any app whose name matches *chrom*, except
com.android.chrome, which is the chrome stable package. Doing so also
removes the corresponding dirs under /data/data/ and /data/app/
(2) remove any dir under /data/app-lib/ whose name matches *chrom*
(3) remove any files under /data/tombstones/ whose name matches "tombstone*"
(4) remove /data/local.prop if there is any
(5) remove /data/local/chrome-command-line if there is any
(6) remove anything under /data/local/.config/ if the dir exists
(this is telemetry related)
(7) remove anything under /data/local/tmp/
Arguments:
device: the device to wipe
"""
try:
if device.IsUserBuild():
_UninstallIfMatch(device, _CHROME_PACKAGE_REGEX)
device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(),
shell=True, check_return=True)
device.RunShellCommand('rm -rf /data/local/tmp/*',
shell=True, check_return=True)
else:
device.EnableRoot()
_UninstallIfMatch(device, _CHROME_PACKAGE_REGEX)
_WipeUnderDirIfMatch(device, '/data/app-lib/', _CHROME_PACKAGE_REGEX)
_WipeUnderDirIfMatch(device, '/data/tombstones/', _TOMBSTONE_REGEX)
_WipeFileOrDir(device, '/data/local.prop')
_WipeFileOrDir(device, '/data/local/chrome-command-line')
_WipeFileOrDir(device, '/data/local/.config/')
_WipeFileOrDir(device, '/data/local/tmp/')
device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(),
shell=True, check_return=True)
except device_errors.CommandFailedError:
logger.exception('Possible failure while wiping the device. '
'Attempting to continue.')
def _UninstallIfMatch(device, pattern):
installed_packages = device.RunShellCommand(
['pm', 'list', 'packages'], check_return=True)
installed_system_packages = [
pkg.split(':')[1] for pkg in device.RunShellCommand(
['pm', 'list', 'packages', '-s'], check_return=True)]
for package_output in installed_packages:
package = package_output.split(":")[1]
if pattern.match(package) and package not in installed_system_packages:
device.Uninstall(package)
def _WipeUnderDirIfMatch(device, path, pattern):
for filename in device.ListDirectory(path):
if pattern.match(filename):
_WipeFileOrDir(device, posixpath.join(path, filename))
def _WipeFileOrDir(device, path):
if device.PathExists(path):
device.RunShellCommand(['rm', '-rf', path], check_return=True)
def WipeDevice(device, adb_key_files):
"""Wipes data from device, keeping only the adb_keys for authorization.
After wiping data on a device that has been authorized, adb can still
communicate with the device, but after reboot the device will need to be
re-authorized because the adb keys file is stored in /data/misc/adb/.
Thus, adb_keys file is rewritten so the device does not need to be
re-authorized.
Arguments:
device: the device to wipe
"""
try:
device.EnableRoot()
device_authorized = device.FileExists(adb_wrapper.ADB_KEYS_FILE)
if device_authorized:
adb_keys = device.ReadFile(adb_wrapper.ADB_KEYS_FILE,
as_root=True).splitlines()
device.RunShellCommand(['wipe', 'data'],
as_root=True, check_return=True)
device.adb.WaitForDevice()
if device_authorized:
adb_keys_set = set(adb_keys)
for adb_key_file in adb_key_files or []:
try:
with open(adb_key_file, 'r') as f:
adb_public_keys = f.readlines()
adb_keys_set.update(adb_public_keys)
except IOError:
logger.warning('Unable to find adb keys file %s.', adb_key_file)
_WriteAdbKeysFile(device, '\n'.join(adb_keys_set))
except device_errors.CommandFailedError:
logger.exception('Possible failure while wiping the device. '
'Attempting to continue.')
def _WriteAdbKeysFile(device, adb_keys_string):
dir_path = posixpath.dirname(adb_wrapper.ADB_KEYS_FILE)
device.RunShellCommand(['mkdir', '-p', dir_path],
as_root=True, check_return=True)
device.RunShellCommand(['restorecon', dir_path],
as_root=True, check_return=True)
device.WriteFile(adb_wrapper.ADB_KEYS_FILE, adb_keys_string, as_root=True)
device.RunShellCommand(['restorecon', adb_wrapper.ADB_KEYS_FILE],
as_root=True, check_return=True)
def SetProperties(device, enable_java_debug, disable_location,
disable_mock_location):
try:
device.EnableRoot()
except device_errors.CommandFailedError as e:
logger.warning(str(e))
if not device.IsUserBuild():
_ConfigureLocalProperties(device, enable_java_debug)
else:
logger.warning('Cannot configure properties in user builds.')
settings.ConfigureContentSettings(
device, settings.DETERMINISTIC_DEVICE_SETTINGS)
if disable_location:
settings.ConfigureContentSettings(
device, settings.DISABLE_LOCATION_SETTINGS)
else:
settings.ConfigureContentSettings(
device, settings.ENABLE_LOCATION_SETTINGS)
if disable_mock_location:
settings.ConfigureContentSettings(
device, settings.DISABLE_MOCK_LOCATION_SETTINGS)
else:
settings.ConfigureContentSettings(
device, settings.ENABLE_MOCK_LOCATION_SETTINGS)
settings.SetLockScreenSettings(device)
# Some device types can momentarily disappear after setting properties.
device.adb.WaitForDevice()
def DisableNetwork(device):
settings.ConfigureContentSettings(
device, settings.NETWORK_DISABLED_SETTINGS)
if device.build_version_sdk >= version_codes.MARSHMALLOW:
# Ensure that NFC is also switched off.
device.RunShellCommand(['svc', 'nfc', 'disable'],
as_root=True, check_return=True)
def DisableSystemChrome(device):
# The system chrome version on the device interferes with some tests.
device.RunShellCommand(['pm', 'disable', 'com.android.chrome'],
as_root=True, check_return=True)
def _FindSystemPackagePaths(device, system_package_list):
found_paths = []
for system_package in system_package_list:
found_paths.extend(device.GetApplicationPaths(system_package))
return [p for p in found_paths if p.startswith('/system/')]
def _FindSystemAppPaths(device, system_app_list):
found_paths = []
for system_app in system_app_list:
for directory in _SYSTEM_APP_DIRECTORIES:
path = os.path.join(directory, system_app)
if device.PathExists(path):
found_paths.append(path)
return found_paths
def RemoveSystemApps(
device, system_app_remove_list, system_package_remove_list):
"""Attempts to remove the provided system apps from the given device.
Arguments:
device: The device to remove the system apps from.
system_app_remove_list: A list of app names to remove, e.g.
['WebViewGoogle', 'GoogleVrCore']
system_package_remove_list: A list of app packages to remove, e.g.
['com.google.android.webview']
"""
device.EnableRoot()
if device.HasRoot():
system_app_paths = (
_FindSystemAppPaths(device, system_app_remove_list) +
_FindSystemPackagePaths(device, system_package_remove_list))
if system_app_paths:
# Disable Marshmallow's Verity security feature
if device.build_version_sdk >= version_codes.MARSHMALLOW:
logger.info('Disabling Verity on %s', device.serial)
device.adb.DisableVerity()
device.Reboot()
device.WaitUntilFullyBooted()
device.EnableRoot()
device.adb.Remount()
device.RunShellCommand(['stop'], check_return=True)
device.RemovePath(system_app_paths, force=True, recursive=True)
device.RunShellCommand(['start'], check_return=True)
else:
raise device_errors.CommandFailedError(
'Failed to remove system apps from non-rooted device', str(device))
def _ConfigureLocalProperties(device, java_debug=True):
"""Set standard readonly testing device properties prior to reboot."""
local_props = [
'persist.sys.usb.config=adb',
'ro.monkey=1',
'ro.test_harness=1',
'ro.audio.silent=1',
'ro.setupwizard.mode=DISABLED',
]
if java_debug:
local_props.append(
'%s=all' % device_utils.DeviceUtils.JAVA_ASSERT_PROPERTY)
local_props.append('debug.checkjni=1')
try:
device.WriteFile(
device.LOCAL_PROPERTIES_PATH,
'\n'.join(local_props), as_root=True)
# Android will not respect the local props file if it is world writable.
device.RunShellCommand(
['chmod', '644', device.LOCAL_PROPERTIES_PATH],
as_root=True, check_return=True)
except device_errors.CommandFailedError:
logger.exception('Failed to configure local properties.')
def FinishProvisioning(device):
# The lockscreen can't be disabled on user builds, so send a keyevent
# to unlock it.
if device.IsUserBuild():
device.SendKeyEvent(keyevent.KEYCODE_MENU)
def WaitForCharge(device, min_battery_level):
battery = battery_utils.BatteryUtils(device)
try:
battery.ChargeDeviceToLevel(min_battery_level)
except device_errors.DeviceChargingError:
device.Reboot()
battery.ChargeDeviceToLevel(min_battery_level)
def WaitForBatteryTemperature(device, max_battery_temp):
try:
battery = battery_utils.BatteryUtils(device)
battery.LetBatteryCoolToTemperature(max_battery_temp)
except device_errors.CommandFailedError:
logger.exception('Unable to let battery cool to specified temperature.')
def SetDate(device):
def _set_and_verify_date():
if device.build_version_sdk >= version_codes.MARSHMALLOW:
date_format = '%m%d%H%M%Y.%S'
set_date_command = ['date', '-u']
get_date_command = ['date', '-u']
else:
date_format = '%Y%m%d.%H%M%S'
set_date_command = ['date', '-s']
get_date_command = ['date']
# TODO(jbudorick): This is wrong on pre-M devices -- get/set are
# dealing in local time, but we're setting based on GMT.
strgmtime = time.strftime(date_format, time.gmtime())
set_date_command.append(strgmtime)
device.RunShellCommand(set_date_command, as_root=True, check_return=True)
get_date_command.append('+"%Y%m%d.%H%M%S"')
device_time = device.RunShellCommand(
get_date_command, check_return=True,
as_root=True, single_line=True).replace('"', '')
device_time = datetime.datetime.strptime(device_time, "%Y%m%d.%H%M%S")
correct_time = datetime.datetime.strptime(strgmtime, date_format)
tdelta = (correct_time - device_time).seconds
if tdelta <= 1:
logger.info('Date/time successfully set on %s', device)
return True
else:
logger.error('Date mismatch. Device: %s Correct: %s',
device_time.isoformat(), correct_time.isoformat())
return False
# Sometimes the date is not set correctly on the devices. Retry on failure.
if device.IsUserBuild():
# TODO(bpastene): Figure out how to set the date & time on user builds.
pass
else:
if not timeout_retry.WaitFor(
_set_and_verify_date, wait_period=1, max_tries=2):
raise device_errors.CommandFailedError(
'Failed to set date & time.', device_serial=str(device))
device.EnableRoot()
# The following intent can take a bit to complete when ran shortly after
# device boot-up.
device.BroadcastIntent(
intent.Intent(action='android.intent.action.TIME_SET'),
timeout=180)
def LogDeviceProperties(device):
props = device.RunShellCommand(['getprop'], check_return=True)
for prop in props:
logger.info(' %s', prop)
# TODO(jbudorick): Relocate this either to device_utils or a separate
# and more intentionally reusable layer on top of device_utils.
def CheckExternalStorage(device):
"""Checks that storage is writable and if not makes it writable.
Arguments:
device: The device to check.
"""
try:
with device_temp_file.DeviceTempFile(
device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f:
device.WriteFile(f.name, 'test')
except device_errors.CommandFailedError:
logger.info('External storage not writable. Remounting / as RW')
device.RunShellCommand(['mount', '-o', 'remount,rw', '/'],
check_return=True, as_root=True)
device.EnableRoot()
with device_temp_file.DeviceTempFile(
device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f:
device.WriteFile(f.name, 'test')
def StandaloneVrDeviceSetup(device):
"""Performs any additional setup necessary for standalone Android VR devices.
Arguments:
device: The device to check.
"""
if device.product_name not in _STANDALONE_VR_DEVICES:
return
# Modify VrCore's settings so that any first time setup, etc. is skipped.
shared_pref = shared_prefs.SharedPrefs(device, 'com.google.vr.vrcore',
'VrCoreSettings.xml', use_encrypted_path=True)
shared_pref.Load()
# Skip first time setup.
shared_pref.SetBoolean('DaydreamSetupComplete', True)
# Disable the automatic prompt that shows anytime the device detects that a
# controller isn't connected.
shared_pref.SetBoolean('gConfigFlags:controller_recovery_enabled', False)
# Use an automated controller instead of a real one so we get past the
# controller pairing screen that's shown on startup.
shared_pref.SetBoolean('UseAutomatedController', True)
shared_pref.Commit()
def main(raw_args):
# Recommended options on perf bots:
# --disable-network
# TODO(tonyg): We eventually want network on. However, currently radios
# can cause perfbots to drain faster than they charge.
# --min-battery-level 95
# Some perf bots run benchmarks with USB charging disabled which leads
# to gradual draining of the battery. We must wait for a full charge
# before starting a run in order to keep the devices online.
parser = argparse.ArgumentParser(
description='Provision Android devices with settings required for bots.')
logging_common.AddLoggingArguments(parser)
script_common.AddDeviceArguments(parser)
script_common.AddEnvironmentArguments(parser)
parser.add_argument(
'--adb-key-files', type=str, nargs='+',
help='list of adb keys to push to device')
parser.add_argument(
'--disable-location', action='store_true',
help='disable Google location services on devices')
parser.add_argument(
'--disable-mock-location', action='store_true', default=False,
help='Set ALLOW_MOCK_LOCATION to false')
parser.add_argument(
'--disable-network', action='store_true',
help='disable network access on devices')
parser.add_argument(
'--disable-java-debug', action='store_false',
dest='enable_java_debug', default=True,
help='disable Java property asserts and JNI checking')
parser.add_argument(
'--disable-system-chrome', action='store_true',
help='DEPRECATED: use --remove-system-packages com.android.google '
'Disable the system chrome from devices.')
parser.add_argument(
'--emulators', action='store_true',
help='provision only emulators and ignore usb devices '
'(this will not wipe emulators)')
parser.add_argument(
'--max-battery-temp', type=int, metavar='NUM',
help='Wait for the battery to have this temp or lower.')
parser.add_argument(
'--min-battery-level', type=int, metavar='NUM',
help='wait for the device to reach this minimum battery'
' level before trying to continue')
parser.add_argument(
'--output-device-blacklist',
help='Json file to output the device blacklist.')
parser.add_argument(
'--reboot-timeout', metavar='SECS', type=int,
help='when wiping the device, max number of seconds to'
' wait after each reboot '
'(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT)
parser.add_argument(
'--remove-system-apps', nargs='*', dest='system_app_remove_list',
help='DEPRECATED: use --remove-system-packages instead. '
'The names of system apps to remove. ')
parser.add_argument(
'--remove-system-packages', nargs='*', dest='system_package_remove_list',
help='The names of system packages to remove.')
parser.add_argument(
'--remove-system-webview', action='store_true',
help='DEPRECATED: use --remove-system-packages '
'com.google.android.webview com.android.webview '
'Remove the system webview from devices.')
parser.add_argument(
'--skip-wipe', action='store_true', default=False,
help='do not wipe device data during provisioning')
# No-op arguments for compatibility with build/android/provision_devices.py.
# TODO(jbudorick): Remove these once all callers have stopped using them.
parser.add_argument(
'--chrome-specific-wipe', action='store_true',
help=argparse.SUPPRESS)
parser.add_argument(
'--phase', action='append',
help=argparse.SUPPRESS)
parser.add_argument(
'-r', '--auto-reconnect', action='store_true',
help=argparse.SUPPRESS)
parser.add_argument(
'-t', '--target',
help=argparse.SUPPRESS)
args = parser.parse_args(raw_args)
logging_common.InitializeLogging(args)
script_common.InitializeEnvironment(args)
try:
return ProvisionDevices(
args.devices,
args.blacklist_file,
adb_key_files=args.adb_key_files,
disable_location=args.disable_location,
disable_mock_location=args.disable_mock_location,
disable_network=args.disable_network,
disable_system_chrome=args.disable_system_chrome,
emulators=args.emulators,
enable_java_debug=args.enable_java_debug,
max_battery_temp=args.max_battery_temp,
min_battery_level=args.min_battery_level,
output_device_blacklist=args.output_device_blacklist,
reboot_timeout=args.reboot_timeout,
remove_system_webview=args.remove_system_webview,
system_app_remove_list=args.system_app_remove_list,
system_package_remove_list=args.system_package_remove_list,
wipe=not args.skip_wipe and not args.emulators)
except (device_errors.DeviceUnreachableError, device_errors.NoDevicesError):
logging.exception('Unable to provision local devices.')
return exit_codes.INFRA
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python
# 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.
"""Takes a screenshot from an Android device."""
import argparse
import logging
import os
import sys
if __name__ == '__main__':
sys.path.append(os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..')))
from devil.android import device_utils
from devil.android.tools import script_common
from devil.utils import logging_common
logger = logging.getLogger(__name__)
def main():
# Parse options.
parser = argparse.ArgumentParser(description=__doc__)
logging_common.AddLoggingArguments(parser)
script_common.AddDeviceArguments(parser)
parser.add_argument('-f', '--file', metavar='FILE',
help='Save result to file instead of generating a '
'timestamped file name.')
parser.add_argument('host_file', nargs='?',
help='File to which the screenshot will be saved.')
args = parser.parse_args()
host_file = args.host_file or args.file
logging_common.InitializeLogging(args)
devices = script_common.GetDevices(args.devices, args.blacklist_file)
def screenshot(device):
f = None
if host_file:
root, ext = os.path.splitext(host_file)
f = '%s_%s%s' % (root, str(device), ext)
f = device.TakeScreenshot(f)
print 'Screenshot for device %s written to %s' % (
str(device), os.path.abspath(f))
device_utils.DeviceUtils.parallel(devices).pMap(screenshot)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,87 @@
# 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 import devil_env
from devil.android import device_blacklist
from devil.android import device_errors
from devil.android import device_utils
def AddEnvironmentArguments(parser):
"""Adds environment-specific arguments to the provided parser.
After adding these arguments, you must pass the user-specified values when
initializing devil. See the InitializeEnvironment() to determine how to do so.
Args:
parser: an instance of argparse.ArgumentParser
"""
parser.add_argument(
'--adb-path', type=os.path.realpath,
help='Path to the adb binary')
def InitializeEnvironment(args):
"""Initializes devil based on the args added by AddEnvironmentArguments().
This initializes devil, and configures it to use the adb binary specified by
the '--adb-path' flag (if provided by the user, otherwise this defaults to
devil's copy of adb). Although this is one possible way to initialize devil,
you should check if your project has prefered ways to initialize devil (ex.
the chromium project uses devil_chromium.Initialize() to have different
defaults for dependencies).
This method requires having previously called AddEnvironmentArguments() on the
relevant argparse.ArgumentParser.
Note: you should only initialize devil once, and subsequent calls to any
method wrapping devil_env.config.Initialize() will have no effect.
Args:
args: the parsed args returned by an argparse.ArgumentParser
"""
devil_dynamic_config = devil_env.EmptyConfig()
if args.adb_path:
devil_dynamic_config['dependencies'].update(
devil_env.LocalConfigItem(
'adb', devil_env.GetPlatform(), args.adb_path))
devil_env.config.Initialize(configs=[devil_dynamic_config])
def AddDeviceArguments(parser):
"""Adds device and blacklist arguments to the provided parser.
Args:
parser: an instance of argparse.ArgumentParser
"""
parser.add_argument(
'-d', '--device', dest='devices', action='append',
help='Serial number of the Android device to use. (default: use all)')
parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
def GetDevices(requested_devices, blacklist_file):
"""Gets a list of healthy devices matching the given parameters."""
if not isinstance(blacklist_file, device_blacklist.Blacklist):
blacklist_file = (device_blacklist.Blacklist(blacklist_file)
if blacklist_file
else None)
devices = device_utils.DeviceUtils.HealthyDevices(blacklist_file)
if not devices:
raise device_errors.NoDevicesError()
elif requested_devices:
requested = set(requested_devices)
available = set(str(d) for d in devices)
missing = requested.difference(available)
if missing:
raise device_errors.DeviceUnreachableError(next(iter(missing)))
return sorted(device_utils.DeviceUtils(d)
for d in available.intersection(requested))
else:
return devices

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python
# 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 argparse
import sys
import tempfile
import unittest
from devil import devil_env
from devil.android import device_errors
from devil.android import device_utils
from devil.android.tools import script_common
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
with devil_env.SysPath(devil_env.DEPENDENCY_MANAGER_PATH):
# pylint: disable=wrong-import-order
from dependency_manager import exceptions
class GetDevicesTest(unittest.TestCase):
def testNoSpecs(self):
devices = [
device_utils.DeviceUtils('123'),
device_utils.DeviceUtils('456'),
]
with mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices',
return_value=devices):
self.assertEquals(
devices,
script_common.GetDevices(None, None))
def testWithDevices(self):
devices = [
device_utils.DeviceUtils('123'),
device_utils.DeviceUtils('456'),
]
with mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices',
return_value=devices):
self.assertEquals(
[device_utils.DeviceUtils('456')],
script_common.GetDevices(['456'], None))
def testMissingDevice(self):
with mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices',
return_value=[device_utils.DeviceUtils('123')]):
with self.assertRaises(device_errors.DeviceUnreachableError):
script_common.GetDevices(['456'], None)
def testNoDevices(self):
with mock.patch('devil.android.device_utils.DeviceUtils.HealthyDevices',
return_value=[]):
with self.assertRaises(device_errors.NoDevicesError):
script_common.GetDevices(None, None)
class InitializeEnvironmentTest(unittest.TestCase):
def setUp(self):
# pylint: disable=protected-access
self.parser = argparse.ArgumentParser()
script_common.AddEnvironmentArguments(self.parser)
devil_env.config = devil_env._Environment()
def testNoAdb(self):
args = self.parser.parse_args([])
script_common.InitializeEnvironment(args)
with self.assertRaises(exceptions.NoPathFoundError):
devil_env.config.LocalPath('adb')
def testAdb(self):
with tempfile.NamedTemporaryFile() as f:
args = self.parser.parse_args(['--adb-path=%s' % f.name])
script_common.InitializeEnvironment(args)
self.assertEquals(
f.name,
devil_env.config.LocalPath('adb'))
def testNonExistentAdb(self):
with tempfile.NamedTemporaryFile() as f:
args = self.parser.parse_args(['--adb-path=%s' % f.name])
script_common.InitializeEnvironment(args)
with self.assertRaises(exceptions.NoPathFoundError):
devil_env.config.LocalPath('adb')
if __name__ == '__main__':
sys.exit(unittest.main())

View File

@@ -0,0 +1,273 @@
#!/usr/bin/env python
# Copyright 2017 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 script to replace a system app while running a command."""
import argparse
import contextlib
import logging
import os
import posixpath
import re
import sys
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import apk_helper
from devil.android import device_errors
from devil.android import device_temp_file
from devil.android.sdk import version_codes
from devil.android.tools import script_common
from devil.utils import cmd_helper
from devil.utils import parallelizer
from devil.utils import run_tests_helper
logger = logging.getLogger(__name__)
# Some system apps aren't actually installed in the /system/ directory, so
# special case them here with the correct install location.
SPECIAL_SYSTEM_APP_LOCATIONS = {
# This also gets installed in /data/app when not a system app, so this script
# will remove either version. This doesn't appear to cause any issues, but
# will cause a few unnecessary reboots if this is the only package getting
# removed and it's already not a system app.
'com.google.ar.core': '/data/app/',
}
# Gets app path and package name pm list packages -f output.
_PM_LIST_PACKAGE_PATH_RE = re.compile(r'^\s*package:(\S+)=(\S+)\s*$')
def RemoveSystemApps(device, package_names):
"""Removes the given system apps.
Args:
device: (device_utils.DeviceUtils) the device for which the given
system app should be removed.
package_name: (iterable of strs) the names of the packages to remove.
"""
system_package_paths = _FindSystemPackagePaths(device, package_names)
if system_package_paths:
with EnableSystemAppModification(device):
device.RemovePath(system_package_paths, force=True, recursive=True)
@contextlib.contextmanager
def ReplaceSystemApp(device, package_name, replacement_apk,
install_timeout=None):
"""A context manager that replaces the given system app while in scope.
Args:
device: (device_utils.DeviceUtils) the device for which the given
system app should be replaced.
package_name: (str) the name of the package to replace.
replacement_apk: (str) the path to the APK to use as a replacement.
"""
storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb)
relocate_app = _RelocateApp(device, package_name, storage_dir.name)
install_app = _TemporarilyInstallApp(device, replacement_apk, install_timeout)
with storage_dir, relocate_app, install_app:
yield
def _FindSystemPackagePaths(device, system_package_list):
"""Finds all system paths for the given packages."""
found_paths = []
for system_package in system_package_list:
paths = _GetApplicationPaths(device, system_package)
p = _GetSystemPath(system_package, paths)
if p:
found_paths.append(p)
return found_paths
# Find all application paths, even those flagged as uninstalled, as these
# would still block another package with the same name from installation
# if they differ in signing keys.
# TODO(aluo): Move this into device_utils.py
def _GetApplicationPaths(device, package):
paths = []
lines = device.RunShellCommand(['pm', 'list', 'packages', '-f', '-u',
package], check_return=True)
for line in lines:
match = re.match(_PM_LIST_PACKAGE_PATH_RE, line)
if match:
path = match.group(1)
package_name = match.group(2)
if package_name == package:
paths.append(path)
return paths
def _GetSystemPath(package, paths):
for p in paths:
if p.startswith(SPECIAL_SYSTEM_APP_LOCATIONS.get(package, '/system/')):
return p
return None
_ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps'
@contextlib.contextmanager
def EnableSystemAppModification(device):
"""A context manager that allows system apps to be modified while in scope.
Args:
device: (device_utils.DeviceUtils) the device
"""
if device.GetProp(_ENABLE_MODIFICATION_PROP) == '1':
yield
return
# All calls that could potentially need root should run with as_root=True, but
# it looks like some parts of Telemetry work as-is by implicitly assuming that
# root is already granted if it's necessary. The reboot can mess with this, so
# as a workaround, check whether we're starting with root already, and if so,
# restore the device to that state at the end.
should_restore_root = device.HasRoot()
device.EnableRoot()
if not device.HasRoot():
raise device_errors.CommandFailedError(
'Failed to enable modification of system apps on non-rooted device',
str(device))
try:
# Disable Marshmallow's Verity security feature
if device.build_version_sdk >= version_codes.MARSHMALLOW:
logger.info('Disabling Verity on %s', device.serial)
device.adb.DisableVerity()
device.Reboot()
device.WaitUntilFullyBooted()
device.EnableRoot()
device.adb.Remount()
device.RunShellCommand(['stop'], check_return=True)
device.SetProp(_ENABLE_MODIFICATION_PROP, '1')
yield
finally:
device.SetProp(_ENABLE_MODIFICATION_PROP, '0')
device.Reboot()
device.WaitUntilFullyBooted()
if should_restore_root:
device.EnableRoot()
@contextlib.contextmanager
def _RelocateApp(device, package_name, relocate_to):
"""A context manager that relocates an app while in scope."""
relocation_map = {}
system_package_paths = _FindSystemPackagePaths(device, [package_name])
if system_package_paths:
relocation_map = {
p: posixpath.join(relocate_to, posixpath.relpath(p, '/'))
for p in system_package_paths
}
relocation_dirs = [
posixpath.dirname(d)
for _, d in relocation_map.iteritems()
]
device.RunShellCommand(['mkdir', '-p'] + relocation_dirs,
check_return=True)
_MoveApp(device, relocation_map)
else:
logger.info('No system package "%s"', package_name)
try:
yield
finally:
_MoveApp(device, {v: k for k, v in relocation_map.iteritems()})
@contextlib.contextmanager
def _TemporarilyInstallApp(device, apk, install_timeout=None):
"""A context manager that installs an app while in scope."""
if install_timeout is None:
device.Install(apk, reinstall=True)
else:
device.Install(apk, reinstall=True, timeout=install_timeout)
try:
yield
finally:
device.Uninstall(apk_helper.GetPackageName(apk))
def _MoveApp(device, relocation_map):
"""Moves an app according to the provided relocation map.
Args:
device: (device_utils.DeviceUtils)
relocation_map: (dict) A dict that maps src to dest
"""
movements = [
'mv %s %s' % (k, v)
for k, v in relocation_map.iteritems()
]
cmd = ' && '.join(movements)
with EnableSystemAppModification(device):
device.RunShellCommand(cmd, as_root=True, check_return=True, shell=True)
def main(raw_args):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
def add_common_arguments(p):
script_common.AddDeviceArguments(p)
script_common.AddEnvironmentArguments(p)
p.add_argument(
'-v', '--verbose', action='count', default=0,
help='Print more information.')
p.add_argument('command', nargs='*')
@contextlib.contextmanager
def remove_system_app(device, args):
RemoveSystemApps(device, args.packages)
yield
remove_parser = subparsers.add_parser('remove')
remove_parser.add_argument(
'--package', dest='packages', nargs='*', required=True,
help='The system package(s) to remove.')
add_common_arguments(remove_parser)
remove_parser.set_defaults(func=remove_system_app)
@contextlib.contextmanager
def replace_system_app(device, args):
with ReplaceSystemApp(device, args.package, args.replace_with):
yield
replace_parser = subparsers.add_parser('replace')
replace_parser.add_argument(
'--package', required=True,
help='The system package to replace.')
replace_parser.add_argument(
'--replace-with', metavar='APK', required=True,
help='The APK with which the existing system app should be replaced.')
add_common_arguments(replace_parser)
replace_parser.set_defaults(func=replace_system_app)
args = parser.parse_args(raw_args)
run_tests_helper.SetLogLevel(args.verbose)
script_common.InitializeEnvironment(args)
devices = script_common.GetDevices(args.devices, args.blacklist_file)
parallel_devices = parallelizer.SyncParallelizer(
[args.func(d, args) for d in devices])
with parallel_devices:
if args.command:
return cmd_helper.Call(args.command)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python
# Copyright 2017 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 posixpath
import shutil
import sys
import tempfile
import unittest
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..')))
from devil import base_error
from devil import devil_env
from devil.android import device_temp_file
from devil.android import device_test_case
from devil.android import device_utils
from devil.android.tools import system_app
logger = logging.getLogger(__name__)
class SystemAppDeviceTest(device_test_case.DeviceTestCase):
PACKAGE = 'com.google.android.webview'
def setUp(self):
super(SystemAppDeviceTest, self).setUp()
self._device = device_utils.DeviceUtils(self.serial)
self._original_paths = self._device.GetApplicationPaths(self.PACKAGE)
self._apk_cache_dir = tempfile.mkdtemp()
# Host location -> device location
self._cached_apks = {}
for o in self._original_paths:
h = os.path.join(self._apk_cache_dir, posixpath.basename(o))
self._device.PullFile(o, h, timeout=60)
self._cached_apks[h] = o
def tearDown(self):
final_paths = self._device.GetApplicationPaths(self.PACKAGE)
if self._original_paths != final_paths:
try:
self._device.Uninstall(self.PACKAGE)
except Exception: # pylint: disable=broad-except
pass
with system_app.EnableSystemAppModification(self._device):
for cached_apk, install_path in self._cached_apks.iteritems():
try:
with device_temp_file.DeviceTempFile(self._device.adb) as tmp:
self._device.adb.Push(cached_apk, tmp.name)
self._device.RunShellCommand(
['mv', tmp.name, install_path],
as_root=True, check_return=True)
except base_error.BaseError:
logger.warning('Failed to reinstall %s',
os.path.basename(cached_apk))
try:
shutil.rmtree(self._apk_cache_dir)
except IOError:
logger.error('Unable to remove app cache directory.')
super(SystemAppDeviceTest, self).tearDown()
def _check_preconditions(self):
if not self._original_paths:
self.skipTest('%s is not installed on %s' % (
self.PACKAGE, str(self._device)))
if not any(p.startswith('/system/') for p in self._original_paths):
self.skipTest('%s is not installed in a system location on %s' % (
self.PACKAGE, str(self._device)))
def testReplace(self):
self._check_preconditions()
replacement = devil_env.config.FetchPath(
'empty_system_webview', device=self._device)
with system_app.ReplaceSystemApp(self._device, self.PACKAGE, replacement):
replaced_paths = self._device.GetApplicationPaths(self.PACKAGE)
self.assertNotEqual(self._original_paths, replaced_paths)
restored_paths = self._device.GetApplicationPaths(self.PACKAGE)
self.assertEqual(self._original_paths, restored_paths)
def testRemove(self):
self._check_preconditions()
system_app.RemoveSystemApps(self._device, [self.PACKAGE])
removed_paths = self._device.GetApplicationPaths(self.PACKAGE)
self.assertEqual([], removed_paths)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,135 @@
#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
if __name__ == '__main__':
sys.path.append(os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..')))
from devil import devil_env
from devil.android import device_utils
from devil.android.sdk import adb_wrapper
from devil.android.sdk import version_codes
from devil.android.tools import system_app
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock
_PACKAGE_NAME = 'com.android'
_PACKAGE_PATH = '/path/to/com.android.apk'
_PM_LIST_PACKAGES_COMMAND = ['pm', 'list', 'packages', '-f', '-u',
_PACKAGE_NAME]
_PM_LIST_PACKAGES_OUTPUT_WITH_PATH = ['package:/path/to/other=' + _PACKAGE_NAME
+ '.other', 'package:' + _PACKAGE_PATH +
'=' + _PACKAGE_NAME]
_PM_LIST_PACKAGES_OUTPUT_WITHOUT_PATH = ['package:/path/to/other=' +
_PACKAGE_NAME + '.other']
class SystemAppTest(unittest.TestCase):
def testDoubleEnableModification(self):
"""Ensures that system app modification logic isn't repeated.
If EnableSystemAppModification uses are nested, inner calls should
not need to perform any of the expensive modification logic.
"""
# pylint: disable=no-self-use,protected-access
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
type(mock_device).build_version_sdk = mock.PropertyMock(
return_value=version_codes.LOLLIPOP)
system_props = {}
def dict_setprop(prop_name, value):
system_props[prop_name] = value
def dict_getprop(prop_name):
return system_props.get(prop_name, '')
mock_device.SetProp.side_effect = dict_setprop
mock_device.GetProp.side_effect = dict_getprop
with system_app.EnableSystemAppModification(mock_device):
mock_device.EnableRoot.assert_called_once_with()
mock_device.GetProp.assert_called_once_with(
system_app._ENABLE_MODIFICATION_PROP)
mock_device.SetProp.assert_called_once_with(
system_app._ENABLE_MODIFICATION_PROP, '1')
mock_device.reset_mock()
with system_app.EnableSystemAppModification(mock_device):
self.assertFalse(mock_device.EnableRoot.mock_calls) # assert not called
mock_device.GetProp.assert_called_once_with(
system_app._ENABLE_MODIFICATION_PROP)
self.assertFalse(mock_device.SetProp.mock_calls) # assert not called
mock_device.reset_mock()
mock_device.SetProp.assert_called_once_with(
system_app._ENABLE_MODIFICATION_PROP, '0')
def test_GetApplicationPaths_found(self):
"""Path found in output along with another package having similar name."""
# pylint: disable=protected-access
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.RunShellCommand.configure_mock(
return_value=_PM_LIST_PACKAGES_OUTPUT_WITH_PATH
)
paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
self.assertEquals([_PACKAGE_PATH], paths)
mock_device.RunShellCommand.assert_called_once_with(
_PM_LIST_PACKAGES_COMMAND, check_return=True)
def test_GetApplicationPaths_notFound(self):
"""Path not found in output, only another package with similar name."""
# pylint: disable=protected-access
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.RunShellCommand.configure_mock(
return_value=_PM_LIST_PACKAGES_OUTPUT_WITHOUT_PATH
)
paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
self.assertEquals([], paths)
mock_device.RunShellCommand.assert_called_once_with(
_PM_LIST_PACKAGES_COMMAND, check_return=True)
def test_GetApplicationPaths_noPaths(self):
"""Nothing containing text of package name found in output."""
# pylint: disable=protected-access
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.RunShellCommand.configure_mock(
return_value=[]
)
paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
self.assertEquals([], paths)
mock_device.RunShellCommand.assert_called_once_with(
_PM_LIST_PACKAGES_COMMAND, check_return=True)
def test_GetApplicationPaths_emptyName(self):
"""Called with empty name, should not return any packages."""
# pylint: disable=protected-access
mock_device = mock.Mock(spec=device_utils.DeviceUtils)
mock_device.RunShellCommand.configure_mock(
return_value=_PM_LIST_PACKAGES_OUTPUT_WITH_PATH
)
paths = system_app._GetApplicationPaths(mock_device, '')
self.assertEquals([], paths)
mock_device.RunShellCommand.assert_called_once_with(
_PM_LIST_PACKAGES_COMMAND[:-1] + [''], check_return=True)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,147 @@
#!/usr/bin/env python
# Copyright 2017 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 script to open the unlock bootloader on-screen prompt on all devices."""
import argparse
import logging
import os
import subprocess
import sys
import time
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil import devil_env
from devil.android import device_errors
from devil.android.sdk import adb_wrapper
from devil.android.sdk import fastboot
from devil.android.tools import script_common
from devil.utils import parallelizer
def reboot_into_bootloader(filter_devices):
# Reboot all devices into bootloader if they aren't there already.
rebooted_devices = set()
for d in adb_wrapper.AdbWrapper.Devices(desired_state=None):
if filter_devices and str(d) not in filter_devices:
continue
state = d.GetState()
if state == 'device':
logging.info('Booting %s to bootloader.', d)
try:
d.Reboot(to_bootloader=True)
rebooted_devices.add(str(d))
except (device_errors.AdbCommandFailedError,
device_errors.DeviceUnreachableError):
logging.exception('Unable to reboot device %s', d)
else:
logging.error('Unable to reboot device %s: %s', d, state)
# Wait for the rebooted devices to show up in fastboot.
if rebooted_devices:
logging.info('Waiting for devices to reboot...')
timeout = 60
start = time.time()
while True:
time.sleep(5)
fastbooted_devices = set([str(d) for d in fastboot.Fastboot.Devices()])
if rebooted_devices <= set(fastbooted_devices):
logging.info('All devices in fastboot.')
break
if time.time() - start > timeout:
logging.error('Timed out waiting for %s to reboot.',
rebooted_devices - set(fastbooted_devices))
break
def unlock_bootloader(d):
# Unlock the phones.
unlocking_processes = []
logging.info('Unlocking %s...', d)
# The command to unlock the bootloader could be either of the following
# depending on the android version and/or oem. Can't really tell which is
# needed, so just try both.
# pylint: disable=protected-access
cmd_old = [d._fastboot_path.read(), '-s', str(d), 'oem', 'unlock']
cmd_new = [d._fastboot_path.read(), '-s', str(d), 'flashing', 'unlock']
unlocking_processes.append(
subprocess.Popen(
cmd_old, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
unlocking_processes.append(
subprocess.Popen(
cmd_new, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
# Give the unlocking command time to finish and/or open the on-screen prompt.
logging.info('Sleeping for 5 seconds...')
time.sleep(5)
leftover_pids = []
for p in unlocking_processes:
p.poll()
rc = p.returncode
# If the command succesfully opened the unlock prompt on the screen, the
# fastboot command process will hang and wait for a response. We still
# need to read its stdout/stderr, so use os.read so that we don't
# have to wait for EOF to be written.
out = os.read(p.stderr.fileno(), 1024).strip().lower()
if not rc:
if out == '...' or out == '< waiting for device >':
logging.info('Device %s is waiting for confirmation.', d)
else:
logging.error(
'Device %s is hanging, but not waiting for confirmation: %s',
d, out)
leftover_pids.append(p.pid)
else:
if 'unknown command' in out:
# Of the two unlocking commands, this was likely the wrong one.
continue
elif 'already unlocked' in out:
logging.info('Device %s already unlocked.', d)
elif 'unlock is not allowed' in out:
logging.error("Device %s is oem locked. Can't unlock bootloader.", d)
return 1
else:
logging.error('Device %s in unknown state: "%s"', d, out)
return 1
break
if leftover_pids:
logging.warning('Processes %s left over after unlocking.', leftover_pids)
return 0
def main():
logging.getLogger().setLevel(logging.INFO)
parser = argparse.ArgumentParser()
script_common.AddDeviceArguments(parser)
parser.add_argument('--adb-path',
help='Absolute path to the adb binary to use.')
args = parser.parse_args()
devil_dynamic_config = devil_env.EmptyConfig()
if args.adb_path:
devil_dynamic_config['dependencies'].update(
devil_env.LocalConfigItem(
'adb', devil_env.GetPlatform(), args.adb_path))
devil_env.config.Initialize(configs=[devil_dynamic_config])
reboot_into_bootloader(args.devices)
devices = [
d for d in fastboot.Fastboot.Devices() if not args.devices or
str(d) in args.devices]
parallel_devices = parallelizer.Parallelizer(devices)
parallel_devices.pMap(unlock_bootloader).pGet(None)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python
# 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.
"""Captures a video from an Android device."""
import argparse
import logging
import os
import threading
import time
import sys
if __name__ == '__main__':
sys.path.append(os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..', '..')))
from devil.android import device_signal
from devil.android import device_utils
from devil.android.tools import script_common
from devil.utils import cmd_helper
from devil.utils import reraiser_thread
from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
class VideoRecorder(object):
"""Records a screen capture video from an Android Device (KitKat or newer)."""
def __init__(self, device, megabits_per_second=4, size=None,
rotate=False):
"""Creates a VideoRecorder instance.
Args:
device: DeviceUtils instance.
host_file: Path to the video file to store on the host.
megabits_per_second: Video bitrate in megabits per second. Allowed range
from 0.1 to 100 mbps.
size: Video frame size tuple (width, height) or None to use the device
default.
rotate: If True, the video will be rotated 90 degrees.
"""
self._bit_rate = megabits_per_second * 1000 * 1000
self._device = device
self._device_file = (
'%s/screen-recording.mp4' % device.GetExternalStoragePath())
self._recorder_thread = None
self._rotate = rotate
self._size = size
self._started = threading.Event()
def __enter__(self):
self.Start()
def Start(self, timeout=None):
"""Start recording video."""
def screenrecord_started():
return bool(self._device.GetPids('screenrecord'))
if screenrecord_started():
raise Exception("Can't run multiple concurrent video captures.")
self._started.clear()
self._recorder_thread = reraiser_thread.ReraiserThread(self._Record)
self._recorder_thread.start()
timeout_retry.WaitFor(
screenrecord_started, wait_period=1, max_tries=timeout)
self._started.wait(timeout)
def _Record(self):
cmd = ['screenrecord', '--verbose', '--bit-rate', str(self._bit_rate)]
if self._rotate:
cmd += ['--rotate']
if self._size:
cmd += ['--size', '%dx%d' % self._size]
cmd += [self._device_file]
for line in self._device.adb.IterShell(
' '.join(cmd_helper.SingleQuote(i) for i in cmd), None):
if line.startswith('Content area is '):
self._started.set()
def __exit__(self, _exc_type, _exc_value, _traceback):
self.Stop()
def Stop(self):
"""Stop recording video."""
if not self._device.KillAll('screenrecord', signum=device_signal.SIGINT,
quiet=True):
logger.warning('Nothing to kill: screenrecord was not running')
self._recorder_thread.join()
def Pull(self, host_file=None):
"""Pull resulting video file from the device.
Args:
host_file: Path to the video file to store on the host.
Returns:
Output video file name on the host.
"""
# TODO(jbudorick): Merge filename generation with the logic for doing so in
# DeviceUtils.
host_file_name = (
host_file
or 'screen-recording-%s-%s.mp4' % (
str(self._device),
time.strftime('%Y%m%dT%H%M%S', time.localtime())))
host_file_name = os.path.abspath(host_file_name)
self._device.PullFile(self._device_file, host_file_name)
self._device.RemovePath(self._device_file, force=True)
return host_file_name
def main():
# Parse options.
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-d', '--device', dest='devices', action='append',
help='Serial number of Android device to use.')
parser.add_argument('--blacklist-file', help='Device blacklist JSON file.')
parser.add_argument('-f', '--file', metavar='FILE',
help='Save result to file instead of generating a '
'timestamped file name.')
parser.add_argument('-v', '--verbose', action='store_true',
help='Verbose logging.')
parser.add_argument('-b', '--bitrate', default=4, type=float,
help='Bitrate in megabits/s, from 0.1 to 100 mbps, '
'%(default)d mbps by default.')
parser.add_argument('-r', '--rotate', action='store_true',
help='Rotate video by 90 degrees.')
parser.add_argument('-s', '--size', metavar='WIDTHxHEIGHT',
help='Frame size to use instead of the device '
'screen size.')
parser.add_argument('host_file', nargs='?',
help='File to which the video capture will be written.')
args = parser.parse_args()
host_file = args.host_file or args.file
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
size = (tuple(int(i) for i in args.size.split('x'))
if args.size
else None)
def record_video(device, stop_recording):
recorder = VideoRecorder(
device, megabits_per_second=args.bitrate, size=size, rotate=args.rotate)
with recorder:
stop_recording.wait()
f = None
if host_file:
root, ext = os.path.splitext(host_file)
f = '%s_%s%s' % (root, str(device), ext)
f = recorder.Pull(f)
print 'Video written to %s' % os.path.abspath(f)
parallel_devices = device_utils.DeviceUtils.parallel(
script_common.GetDevices(args.devices, args.blacklist_file),
async=True)
stop_recording = threading.Event()
running_recording = parallel_devices.pMap(record_video, stop_recording)
print 'Recording. Press Enter to stop.',
sys.stdout.flush()
raw_input()
stop_recording.set()
running_recording.pGet(None)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python
# Copyright 2016 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.
"""Waits for the given devices to be available."""
import argparse
import os
import sys
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
from devil.android import device_utils
from devil.android.tools import script_common
from devil.utils import run_tests_helper
def main(raw_args):
parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='count', help='Log more.')
parser.add_argument('-t', '--timeout', default=30, type=int,
help='Seconds to wait for the devices.')
parser.add_argument('--adb-path', help='ADB binary to use.')
parser.add_argument('device_serials', nargs='*', metavar='SERIAL',
help='Serials of the devices to wait for.')
args = parser.parse_args(raw_args)
run_tests_helper.SetLogLevel(args.verbose)
script_common.InitializeEnvironment(args)
devices = device_utils.DeviceUtils.HealthyDevices(
device_arg=args.device_serials)
parallel_devices = device_utils.DeviceUtils.parallel(devices)
parallel_devices.WaitUntilFullyBooted(timeout=args.timeout)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python
# Copyright 2019 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 script to use a package as the WebView provider while running a command."""
import argparse
import contextlib
import logging
import os
import re
import sys
if __name__ == '__main__':
sys.path.append(
os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..')))
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', '..', '..', '..', 'common', 'py_utils')))
from devil.android import apk_helper
from devil.android import device_errors
from devil.android.sdk import version_codes
from devil.android.tools import script_common
from devil.android.tools import system_app
from devil.utils import cmd_helper
from devil.utils import parallelizer
from devil.utils import run_tests_helper
from py_utils import tempfile_ext
logger = logging.getLogger(__name__)
_SYSTEM_PATH_RE = re.compile(r'^\s*\/system\/')
_WEBVIEW_INSTALL_TIMEOUT = 300
@contextlib.contextmanager
def UseWebViewProvider(device, apk, expected_package=''):
"""A context manager that uses the apk as the webview provider while in scope.
Args:
device: (device_utils.DeviceUtils) The device for which the webview apk
should be used as the provider.
apk: (str) The path to the webview APK to use.
expected_package: (str) If non-empty, verify apk's package name matches
this value.
"""
package_name = apk_helper.GetPackageName(apk)
if expected_package:
if package_name != expected_package:
raise device_errors.CommandFailedError(
'WebView Provider package %s does not match expected %s' %
(package_name, expected_package), str(device))
if (device.build_version_sdk in
[version_codes.NOUGAT, version_codes.NOUGAT_MR1]):
logger.warning('Due to webviewupdate bug in Nougat, WebView Fallback Logic '
'will be disabled and WebView provider may be changed after '
'exit of UseWebViewProvider context manager scope.')
webview_update = device.GetWebViewUpdateServiceDump()
original_fallback_logic = webview_update.get('FallbackLogicEnabled', None)
original_provider = webview_update.get('CurrentWebViewPackage', None)
# This is only necessary if the provider is a fallback provider, but we can't
# generally determine this, so we set this just in case.
device.SetWebViewFallbackLogic(False)
try:
# If user installed versions of the package is present, they must be
# uninstalled first, so that the system version of the package,
# if any, can be found by the ReplaceSystemApp context manager
with _UninstallNonSystemApp(device, package_name):
all_paths = device.GetApplicationPaths(package_name)
system_paths = _FilterPaths(all_paths, True)
non_system_paths = _FilterPaths(all_paths, False)
if non_system_paths:
raise device_errors.CommandFailedError(
'Non-System application paths found after uninstallation: ',
str(non_system_paths))
elif system_paths:
# app is system app, use ReplaceSystemApp to install
with system_app.ReplaceSystemApp(
device,
package_name,
apk,
install_timeout=_WEBVIEW_INSTALL_TIMEOUT):
_SetWebViewProvider(device, package_name)
yield
else:
# app is not present on device, can directly install
with _InstallApp(device, apk):
_SetWebViewProvider(device, package_name)
yield
finally:
# restore the original provider only if it was known and not the current
# provider
if original_provider is not None:
webview_update = device.GetWebViewUpdateServiceDump()
new_provider = webview_update.get('CurrentWebViewPackage', None)
if new_provider != original_provider:
device.SetWebViewImplementation(original_provider)
# enable the fallback logic only if it was known to be enabled
if original_fallback_logic is True:
device.SetWebViewFallbackLogic(True)
def _SetWebViewProvider(device, package_name):
""" Set the WebView provider to the package_name if supported. """
if device.build_version_sdk >= version_codes.NOUGAT:
device.SetWebViewImplementation(package_name)
def _FilterPaths(path_list, is_system):
""" Return paths in the path_list that are/aren't system paths. """
return [
p for p in path_list if is_system == bool(re.match(_SYSTEM_PATH_RE, p))
]
def _RebasePath(new_root, old_root):
""" Graft old_root onto new_root and return the result. """
return os.path.join(new_root, os.path.relpath(old_root, '/'))
@contextlib.contextmanager
def _UninstallNonSystemApp(device, package_name):
""" Make package un-installed while in scope. """
all_paths = device.GetApplicationPaths(package_name)
user_paths = _FilterPaths(all_paths, False)
host_paths = []
if user_paths:
with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
for user_path in user_paths:
host_path = _RebasePath(temp_dir, user_path)
# PullFile takes care of host_path creation if needed.
device.PullFile(user_path, host_path)
host_paths.append(host_path)
device.Uninstall(package_name)
try:
yield
finally:
for host_path in reversed(host_paths):
device.Install(host_path, reinstall=True,
timeout=_WEBVIEW_INSTALL_TIMEOUT)
else:
yield
@contextlib.contextmanager
def _InstallApp(device, apk):
""" Make apk installed while in scope. """
package_name = apk_helper.GetPackageName(apk)
device.Install(apk, reinstall=True, timeout=_WEBVIEW_INSTALL_TIMEOUT)
try:
yield
finally:
device.Uninstall(package_name)
def main(raw_args):
parser = argparse.ArgumentParser()
def add_common_arguments(p):
script_common.AddDeviceArguments(p)
script_common.AddEnvironmentArguments(p)
p.add_argument(
'-v', '--verbose', action='count', default=0,
help='Print more information.')
p.add_argument('command', nargs='*')
@contextlib.contextmanager
def use_webview_provider(device, args):
with UseWebViewProvider(device, args.apk, args.expected_package):
yield
parser.add_argument(
'--apk', required=True,
help='The apk to use as the provider.')
parser.add_argument(
'--expected-package', default='',
help="Verify apk's package name matches value, disabled by default.")
add_common_arguments(parser)
parser.set_defaults(func=use_webview_provider)
args = parser.parse_args(raw_args)
run_tests_helper.SetLogLevel(args.verbose)
script_common.InitializeEnvironment(args)
devices = script_common.GetDevices(args.devices, args.blacklist_file)
parallel_devices = parallelizer.SyncParallelizer(
[args.func(d, args) for d in devices])
with parallel_devices:
if args.command:
return cmd_helper.Call(args.command)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,21 @@
# Copyright (c) 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.
"""
Classes in this package define additional actions that need to be taken to run a
test under some kind of runtime error detection tool.
The interface is intended to be used as follows.
1. For tests that simply run a native process (i.e. no activity is spawned):
Call tool.CopyFiles(device).
Prepend test command line with tool.GetTestWrapper().
2. For tests that spawn an activity:
Call tool.CopyFiles(device).
Call tool.SetupEnvironment().
Run the test as usual.
Call tool.CleanUpEnvironment().
"""

View File

@@ -0,0 +1,53 @@
# Copyright (c) 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 BaseTool(object):
"""A tool that does nothing."""
# pylint: disable=R0201
def __init__(self):
"""Does nothing."""
pass
def GetTestWrapper(self):
"""Returns a string that is to be prepended to the test command line."""
return ''
def GetUtilWrapper(self):
"""Returns the wrapper name for the utilities.
Returns:
A string that is to be prepended to the command line of utility
processes (forwarder, etc.).
"""
return ''
@classmethod
def CopyFiles(cls, device):
"""Copies tool-specific files to the device, create directories, etc."""
pass
def SetupEnvironment(self):
"""Sets up the system environment for a test.
This is a good place to set system properties.
"""
pass
def CleanUpEnvironment(self):
"""Cleans up environment."""
pass
def GetTimeoutScale(self):
"""Returns a multiplier that should be applied to timeout values."""
return 1.0
def NeedsDebugInfo(self):
"""Whether this tool requires debug info.
Returns:
True if this tool can not work with stripped binaries.
"""
return False

View File

@@ -0,0 +1,24 @@
# 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
def __eq__(self, other):
return (self.message == other.message
and self.is_infra_error == other.is_infra_error)
def __ne__(self, other):
return not self == other
@property
def is_infra_error(self):
"""Property to indicate if error was caused by an infrastructure issue."""
return self._is_infra_error

View File

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

View File

@@ -0,0 +1,9 @@
# 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.
"""Common exit codes used by devil."""
ERROR = 1
INFRA = 87
WARNING = 88

View File

@@ -0,0 +1,141 @@
{
"config_type": "BaseConfig",
"dependencies": {
"aapt": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "87bd288daab30624e41faa62aa2c1d5bac3e60aa",
"download_path": "../bin/deps/linux2/x86_64/bin/aapt"
}
}
},
"adb": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "8bd43e3930f6eec643d5dc64cab9e5bb4ddf4909",
"download_path": "../bin/deps/linux2/x86_64/bin/adb"
}
}
},
"android_build_tools_libc++": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "9b986774ad27288a6777ebfa9a08fd8a52003008",
"download_path": "../bin/deps/linux2/x86_64/lib64/libc++.so"
}
}
},
"chromium_commands": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "4e22f641e4757309510e8d9f933f5aa504574ab6",
"download_path": "../bin/deps/linux2/x86_64/lib.java/chromium_commands.dex.jar"
}
}
},
"dexdump": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "c3fdf75afe8eb4062d66703cb556ee1e2064b8ae",
"download_path": "../bin/deps/linux2/x86_64/bin/dexdump"
}
}
},
"empty_system_webview": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"android_arm64-v8a": {
"cloud_storage_hash": "34e583c631a495afbba82ce8a1d4f9b5118a4411",
"download_path": "../bin/deps/android/arm64-v8a/apks/EmptySystemWebView.apk"
},
"android_armeabi-v7a": {
"cloud_storage_hash": "220ff3ba1a6c3c81877997e32784ffd008f293a5",
"download_path": "../bin/deps/android/armeabi-v7a/apks/EmptySystemWebView.apk"
}
}
},
"fastboot": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "db9728166f182800eb9d09e9f036d56e105e8235",
"download_path": "../bin/deps/linux2/x86_64/bin/fastboot"
}
}
},
"forwarder_device": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"android_arm64-v8a": {
"cloud_storage_hash": "f222268d8442979240d1b18de00911a49e548daa",
"download_path": "../bin/deps/android/arm64-v8a/bin/forwarder_device"
},
"android_armeabi-v7a": {
"cloud_storage_hash": "c15267bf01c26eb0aea4f61c780bbba460c5c981",
"download_path": "../bin/deps/android/armeabi-v7a/bin/forwarder_device"
}
}
},
"forwarder_host": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "8fe69994b670f028484eed475dbffc838c8a57f7",
"download_path": "../bin/deps/linux2/x86_64/forwarder_host"
}
}
},
"md5sum_device": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"android_arm64-v8a": {
"cloud_storage_hash": "4e7d2dedd9c6321fdc152b06869e09a3c5817904",
"download_path": "../bin/deps/android/arm64-v8a/bin/md5sum_device"
},
"android_armeabi-v7a": {
"cloud_storage_hash": "39fd90af0f8828202b687f7128393759181c5e2e",
"download_path": "../bin/deps/android/armeabi-v7a/bin/md5sum_device"
},
"android_x86": {
"cloud_storage_hash": "d5cf42ab5986a69c31c0177b0df499d6bf708df6",
"download_path": "../bin/deps/android/x86/bin/md5sum_device"
}
}
},
"md5sum_host": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "4db5bd5e9bea8880d8bf2caa59d0efb0acc19f74",
"download_path": "../bin/deps/linux2/x86_64/bin/md5sum_host"
}
}
},
"split-select": {
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
"cloud_storage_hash": "c116fd0d7ff089561971c078317b75b90f053207",
"download_path": "../bin/deps/linux2/x86_64/bin/split-select"
}
}
}
}
}

View File

@@ -0,0 +1,194 @@
# 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 contextlib
import json
import logging
import os
import platform
import sys
import tempfile
import threading
CATAPULT_ROOT_PATH = os.path.abspath(os.path.join(
os.path.dirname(__file__), '..', '..'))
DEPENDENCY_MANAGER_PATH = os.path.join(
CATAPULT_ROOT_PATH, 'dependency_manager')
PYMOCK_PATH = os.path.join(
CATAPULT_ROOT_PATH, 'third_party', 'mock')
@contextlib.contextmanager
def SysPath(path):
sys.path.append(path)
yield
if sys.path[-1] != path:
sys.path.remove(path)
else:
sys.path.pop()
with SysPath(DEPENDENCY_MANAGER_PATH):
import dependency_manager # pylint: disable=import-error
_ANDROID_BUILD_TOOLS = {'aapt', 'dexdump', 'split-select'}
_DEVIL_DEFAULT_CONFIG = os.path.abspath(os.path.join(
os.path.dirname(__file__), 'devil_dependencies.json'))
_LEGACY_ENVIRONMENT_VARIABLES = {
'ADB_PATH': {
'dependency_name': 'adb',
'platform': 'linux2_x86_64',
},
'ANDROID_SDK_ROOT': {
'dependency_name': 'android_sdk',
'platform': 'linux2_x86_64',
},
}
def EmptyConfig():
return {
'config_type': 'BaseConfig',
'dependencies': {}
}
def LocalConfigItem(dependency_name, dependency_platform, dependency_path):
if isinstance(dependency_path, basestring):
dependency_path = [dependency_path]
return {
dependency_name: {
'file_info': {
dependency_platform: {
'local_paths': dependency_path
},
},
},
}
def _GetEnvironmentVariableConfig():
env_config = EmptyConfig()
path_config = (
(os.environ.get(k), v)
for k, v in _LEGACY_ENVIRONMENT_VARIABLES.iteritems())
path_config = ((p, c) for p, c in path_config if p)
for p, c in path_config:
env_config['dependencies'].update(
LocalConfigItem(c['dependency_name'], c['platform'], p))
return env_config
class _Environment(object):
def __init__(self):
self._dm_init_lock = threading.Lock()
self._dm = None
self._logging_init_lock = threading.Lock()
self._logging_initialized = False
def Initialize(self, configs=None, config_files=None):
"""Initialize devil's environment from configuration files.
This uses all configurations provided via |configs| and |config_files|
to determine the locations of devil's dependencies. Configurations should
all take the form described by py_utils.dependency_manager.BaseConfig.
If no configurations are provided, a default one will be used if available.
Args:
configs: An optional list of dict configurations.
config_files: An optional list of files to load
"""
# Make sure we only initialize self._dm once.
with self._dm_init_lock:
if self._dm is None:
if configs is None:
configs = []
env_config = _GetEnvironmentVariableConfig()
if env_config:
configs.insert(0, env_config)
self._InitializeRecursive(
configs=configs,
config_files=config_files)
assert self._dm is not None, 'Failed to create dependency manager.'
def _InitializeRecursive(self, configs=None, config_files=None):
# This recurses through configs to create temporary files for each and
# take advantage of context managers to appropriately close those files.
# TODO(jbudorick): Remove this recursion if/when dependency_manager
# supports loading configurations directly from a dict.
if configs:
with tempfile.NamedTemporaryFile(delete=False) as next_config_file:
try:
next_config_file.write(json.dumps(configs[0]))
next_config_file.close()
self._InitializeRecursive(
configs=configs[1:],
config_files=[next_config_file.name] + (config_files or []))
finally:
if os.path.exists(next_config_file.name):
os.remove(next_config_file.name)
else:
config_files = config_files or []
if 'DEVIL_ENV_CONFIG' in os.environ:
config_files.append(os.environ.get('DEVIL_ENV_CONFIG'))
config_files.append(_DEVIL_DEFAULT_CONFIG)
self._dm = dependency_manager.DependencyManager(
[dependency_manager.BaseConfig(c) for c in config_files])
def InitializeLogging(self, log_level, formatter=None, handler=None):
if self._logging_initialized:
return
with self._logging_init_lock:
if self._logging_initialized:
return
formatter = formatter or logging.Formatter(
'%(threadName)-4s %(message)s')
handler = handler or logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
devil_logger = logging.getLogger('devil')
devil_logger.setLevel(log_level)
devil_logger.propagate = False
devil_logger.addHandler(handler)
import py_utils.cloud_storage
lock_logger = py_utils.cloud_storage.logger
lock_logger.setLevel(log_level)
lock_logger.propagate = False
lock_logger.addHandler(handler)
self._logging_initialized = True
def FetchPath(self, dependency, arch=None, device=None):
if self._dm is None:
self.Initialize()
if dependency in _ANDROID_BUILD_TOOLS:
self.FetchPath('android_build_tools_libc++', arch=arch, device=device)
return self._dm.FetchPath(dependency, GetPlatform(arch, device))
def LocalPath(self, dependency, arch=None, device=None):
if self._dm is None:
self.Initialize()
return self._dm.LocalPath(dependency, GetPlatform(arch, device))
def PrefetchPaths(self, dependencies=None, arch=None, device=None):
return self._dm.PrefetchPaths(
GetPlatform(arch, device), dependencies=dependencies)
def GetPlatform(arch=None, device=None):
if arch or device:
return 'android_%s' % (arch or device.product_cpu_abi)
return '%s_%s' % (sys.platform, platform.machine())
config = _Environment()

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
# 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=protected-access
import logging
import sys
import unittest
from devil import devil_env
_sys_path_before = list(sys.path)
with devil_env.SysPath(devil_env.PYMOCK_PATH):
_sys_path_with_pymock = list(sys.path)
import mock # pylint: disable=import-error
_sys_path_after = list(sys.path)
class DevilEnvTest(unittest.TestCase):
def testSysPath(self):
self.assertEquals(_sys_path_before, _sys_path_after)
self.assertEquals(
_sys_path_before + [devil_env.PYMOCK_PATH],
_sys_path_with_pymock)
def testGetEnvironmentVariableConfig_configType(self):
with mock.patch('os.environ.get',
mock.Mock(side_effect=lambda _env_var: None)):
env_config = devil_env._GetEnvironmentVariableConfig()
self.assertEquals('BaseConfig', env_config.get('config_type'))
def testGetEnvironmentVariableConfig_noEnv(self):
with mock.patch('os.environ.get',
mock.Mock(side_effect=lambda _env_var: None)):
env_config = devil_env._GetEnvironmentVariableConfig()
self.assertEquals({}, env_config.get('dependencies'))
def testGetEnvironmentVariableConfig_adbPath(self):
def mock_environment(env_var):
return '/my/fake/adb/path' if env_var == 'ADB_PATH' else None
with mock.patch('os.environ.get',
mock.Mock(side_effect=mock_environment)):
env_config = devil_env._GetEnvironmentVariableConfig()
self.assertEquals(
{
'adb': {
'file_info': {
'linux2_x86_64': {
'local_paths': ['/my/fake/adb/path'],
},
},
},
},
env_config.get('dependencies'))
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)

View File

@@ -0,0 +1,501 @@
# 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 codecs
import logging
import os
import pipes
import select
import signal
import string
import StringIO
import subprocess
import sys
import time
from devil import base_error
logger = logging.getLogger(__name__)
_SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
# Cache the string-escape codec to ensure subprocess can find it
# later. Return value doesn't matter.
codecs.lookup('string-escape')
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 ShrinkToSnippet(cmd_parts, var_name, var_value):
"""Constructs a shell snippet for a command using a variable to shrink it.
Takes into account all quoting that needs to happen.
Args:
cmd_parts: A list of command arguments.
var_name: The variable that holds var_value.
var_value: The string to replace in cmd_parts with $var_name
Returns:
A shell snippet that does not include setting the variable.
"""
def shrink(value):
parts = (x and SingleQuote(x) for x in value.split(var_value))
with_substitutions = ('"$%s"' % var_name).join(parts)
return with_substitutions or "''"
return ' '.join(shrink(part) for part in cmd_parts)
def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
# preexec_fn isn't supported on windows.
if sys.platform == 'win32':
close_fds = (stdout is None and stderr is None)
preexec_fn = None
else:
close_fds = True
preexec_fn = lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL)
return subprocess.Popen(
args=args, cwd=cwd, stdout=stdout, stderr=stderr,
shell=shell, close_fds=close_fds, env=env, preexec_fn=preexec_fn)
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.
"""
logger.info(str(args) + ' ' + (cwd or ''))
return Call(args, cwd=cwd)
def GetCmdOutput(args, cwd=None, shell=False, env=None):
"""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.
env: If not None, a mapping that defines environment variables for the
subprocess.
Returns:
Captures and returns the command's stdout.
Prints the command's stderr to logger (which defaults to stdout).
"""
(_, output) = GetCmdStatusAndOutput(args, cwd, shell, env)
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(str(c)) for c in args)
if cwd is None:
cwd = ''
else:
cwd = ':' + cwd
logger.debug('[host]%s> %s', cwd, args)
return args
def GetCmdStatusAndOutput(args, cwd=None, shell=False, env=None):
"""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.
env: If not None, a mapping that defines environment variables for the
subprocess.
Returns:
The 2-tuple (exit code, stdout).
"""
status, stdout, stderr = GetCmdStatusOutputAndError(
args, cwd=cwd, shell=shell, env=env)
if stderr:
logger.critical('STDERR: %s', stderr)
logger.debug('STDOUT: %s%s', stdout[:4096].rstrip(),
'<truncated>' if len(stdout) > 4096 else '')
return (status, stdout)
def StartCmd(args, cwd=None, shell=False, env=None):
"""Starts a subprocess and returns a handle to the process.
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.
env: If not None, a mapping that defines environment variables for the
subprocess.
Returns:
A process handle from subprocess.Popen.
"""
_ValidateAndLogCommand(args, cwd, shell)
return Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=shell, cwd=cwd, env=env)
def GetCmdStatusOutputAndError(args, cwd=None, shell=False, env=None):
"""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.
env: If not None, a mapping that defines environment variables for the
subprocess.
Returns:
The 3-tuple (exit code, stdout, stderr).
"""
_ValidateAndLogCommand(args, cwd, shell)
pipe = Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=shell, cwd=cwd, env=env)
stdout, stderr = pipe.communicate()
return (pipe.returncode, stdout, stderr)
class TimeoutError(base_error.BaseError):
"""Module-specific timeout exception."""
def __init__(self, output=None):
super(TimeoutError, self).__init__('Timeout')
self._output = output
@property
def output(self):
return self._output
def _IterProcessStdoutFcntl(
process, iter_timeout=None, timeout=None, buffer_size=4096,
poll_interval=1):
"""An fcntl-based implementation of _IterProcessStdout."""
# pylint: disable=too-many-nested-blocks
import fcntl
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
iter_end_time = (time.time() + iter_timeout) if iter_timeout else None
while True:
if end_time and time.time() > end_time:
raise TimeoutError()
if iter_end_time and time.time() > iter_end_time:
yield None
iter_end_time = time.time() + iter_timeout
if iter_end_time:
iter_aware_poll_interval = min(
poll_interval,
max(0, iter_end_time - time.time()))
else:
iter_aware_poll_interval = poll_interval
read_fds, _, _ = select.select(
[child_fd], [], [], iter_aware_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:
# If process is closed, keep checking for output data (because of timing
# issues).
while True:
read_fds, _, _ = select.select(
[child_fd], [], [], iter_aware_poll_interval)
if child_fd in read_fds:
data = os.read(child_fd, buffer_size)
if data:
yield data
continue
break
break
finally:
try:
if process.returncode is None:
# Make sure the process doesn't stick around if we fail with an
# exception.
process.kill()
except OSError:
pass
process.wait()
def _IterProcessStdoutQueue(
process, iter_timeout=None, timeout=None, buffer_size=4096,
poll_interval=1):
"""A Queue.Queue-based implementation of _IterProcessStdout.
TODO(jbudorick): Evaluate whether this is a suitable replacement for
_IterProcessStdoutFcntl on all platforms.
"""
# pylint: disable=unused-argument
import Queue
import threading
stdout_queue = Queue.Queue()
def read_process_stdout():
# TODO(jbudorick): Pick an appropriate read size here.
while True:
try:
output_chunk = os.read(process.stdout.fileno(), buffer_size)
except IOError:
break
stdout_queue.put(output_chunk, True)
if not output_chunk and process.poll() is not None:
break
reader_thread = threading.Thread(target=read_process_stdout)
reader_thread.start()
end_time = (time.time() + timeout) if timeout else None
try:
while True:
if end_time and time.time() > end_time:
raise TimeoutError()
try:
s = stdout_queue.get(True, iter_timeout)
if not s:
break
yield s
except Queue.Empty:
yield None
finally:
try:
if process.returncode is None:
# Make sure the process doesn't stick around if we fail with an
# exception.
process.kill()
except OSError:
pass
process.wait()
reader_thread.join()
_IterProcessStdout = (
_IterProcessStdoutQueue
if sys.platform == 'win32'
else _IterProcessStdoutFcntl)
"""Iterate over a process's stdout.
This is intentionally not public.
Args:
process: The process in question.
iter_timeout: An optional length of time, in seconds, to wait in
between each iteration. If no output is received in the given
time, this generator will yield None.
timeout: An optional length of time, in seconds, during which
the process must finish. If it fails to do so, a TimeoutError
will be raised.
buffer_size: The maximum number of bytes to read (and thus yield) at once.
poll_interval: The length of time to wait in calls to `select.select`.
If iter_timeout is set, the remaining length of time in the iteration
may take precedence.
Raises:
TimeoutError: if timeout is set and the process does not complete.
Yields:
basestrings of data or None.
"""
def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
logfile=None, env=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.
env: If not None, a mapping that defines environment variables for the
subprocess.
Returns:
The 2-tuple (exit code, output).
Raises:
TimeoutError on timeout.
"""
_ValidateAndLogCommand(args, cwd, shell)
output = StringIO.StringIO()
process = Popen(args, cwd=cwd, shell=shell, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, env=env)
try:
for data in _IterProcessStdout(process, timeout=timeout):
if logfile:
logfile.write(data)
output.write(data)
except TimeoutError:
raise TimeoutError(output.getvalue())
str_output = output.getvalue()
logger.debug('STDOUT+STDERR: %s%s', str_output[:4096].rstrip(),
'<truncated>' if len(str_output) > 4096 else '')
return process.returncode, str_output
def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None,
shell=False, env=None, 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.
iter_timeout: Timeout for each iteration, in seconds.
timeout: Timeout for the entire command, in seconds.
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.
env: If not None, a mapping that defines environment variables for the
subprocess.
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, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return _IterCmdOutputLines(
process, cmd, iter_timeout=iter_timeout, timeout=timeout,
check_status=check_status)
def _IterCmdOutputLines(process, cmd, iter_timeout=None, timeout=None,
check_status=True):
buffer_output = ''
iter_end = None
cur_iter_timeout = None
if iter_timeout:
iter_end = time.time() + iter_timeout
cur_iter_timeout = iter_timeout
for data in _IterProcessStdout(process, iter_timeout=cur_iter_timeout,
timeout=timeout):
if iter_timeout:
# Check whether the current iteration has timed out.
cur_iter_timeout = iter_end - time.time()
if data is None or cur_iter_timeout < 0:
yield None
iter_end = time.time() + iter_timeout
continue
else:
assert data is not None, (
'Iteration received no data despite no iter_timeout being set. '
'cmd: %s' % cmd)
# Construct lines to yield from raw data.
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 iter_timeout:
iter_end = time.time() + iter_timeout
if buffer_output:
yield buffer_output
if check_status and process.returncode:
raise subprocess.CalledProcessError(process.returncode, cmd)

Some files were not shown because too many files have changed in this diff Show More