Reimport
This commit is contained in:
7
tools/adb/systrace/catapult/devil/devil/__init__.py
Normal file
7
tools/adb/systrace/catapult/devil/devil/__init__.py
Normal 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())
|
||||
@@ -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.
|
||||
384
tools/adb/systrace/catapult/devil/devil/android/apk_helper.py
Normal file
384
tools/adb/systrace/catapult/devil/devil/android/apk_helper.py
Normal 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.')
|
||||
@@ -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)
|
||||
243
tools/adb/systrace/catapult/devil/devil/android/app_ui.py
Normal file
243
tools/adb/systrace/catapult/devil/devil/android/app_ui.py
Normal 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)
|
||||
191
tools/adb/systrace/catapult/devil/devil/android/app_ui_test.py
Normal file
191
tools/adb/systrace/catapult/devil/devil/android/app_ui_test.py
Normal 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')
|
||||
679
tools/adb/systrace/catapult/devil/devil/android/battery_utils.py
Normal file
679
tools/adb/systrace/catapult/devil/devil/android/battery_utils.py
Normal 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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
@@ -0,0 +1,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'),
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
176
tools/adb/systrace/catapult/devil/devil/android/decorators.py
Normal file
176
tools/adb/systrace/catapult/devil/devil/android/decorators.py
Normal 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
196
tools/adb/systrace/catapult/devil/devil/android/device_errors.py
Normal file
196
tools/adb/systrace/catapult/devil/devil/android/device_errors.py
Normal 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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Defines constants for signals that should be supported on devices.
|
||||
|
||||
Note: Obtained by running `kill -l` on a user device.
|
||||
"""
|
||||
|
||||
|
||||
SIGHUP = 1 # Hangup
|
||||
SIGINT = 2 # Interrupt
|
||||
SIGQUIT = 3 # Quit
|
||||
SIGILL = 4 # Illegal instruction
|
||||
SIGTRAP = 5 # Trap
|
||||
SIGABRT = 6 # Aborted
|
||||
SIGBUS = 7 # Bus error
|
||||
SIGFPE = 8 # Floating point exception
|
||||
SIGKILL = 9 # Killed
|
||||
SIGUSR1 = 10 # User signal 1
|
||||
SIGSEGV = 11 # Segmentation fault
|
||||
SIGUSR2 = 12 # User signal 2
|
||||
SIGPIPE = 13 # Broken pipe
|
||||
SIGALRM = 14 # Alarm clock
|
||||
SIGTERM = 15 # Terminated
|
||||
SIGSTKFLT = 16 # Stack fault
|
||||
SIGCHLD = 17 # Child exited
|
||||
SIGCONT = 18 # Continue
|
||||
SIGSTOP = 19 # Stopped (signal)
|
||||
SIGTSTP = 20 # Stopped
|
||||
SIGTTIN = 21 # Stopped (tty input)
|
||||
SIGTTOU = 22 # Stopped (tty output)
|
||||
SIGURG = 23 # Urgent I/O condition
|
||||
SIGXCPU = 24 # CPU time limit exceeded
|
||||
SIGXFSZ = 25 # File size limit exceeded
|
||||
SIGVTALRM = 26 # Virtual timer expired
|
||||
SIGPROF = 27 # Profiling timer expired
|
||||
SIGWINCH = 28 # Window size changed
|
||||
SIGIO = 29 # I/O possible
|
||||
SIGPWR = 30 # Power failure
|
||||
SIGSYS = 31 # Bad system call
|
||||
@@ -0,0 +1,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()
|
||||
@@ -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()
|
||||
3373
tools/adb/systrace/catapult/devil/devil/android/device_utils.py
Normal file
3373
tools/adb/systrace/catapult/devil/devil/android/device_utils.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
3543
tools/adb/systrace/catapult/devil/devil/android/device_utils_test.py
Normal file
3543
tools/adb/systrace/catapult/devil/devil/android/device_utils_test.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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)
|
||||
328
tools/adb/systrace/catapult/devil/devil/android/flag_changer.py
Normal file
328
tools/adb/systrace/catapult/devil/devil/android/flag_changer.py
Normal 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])
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
476
tools/adb/systrace/catapult/devil/devil/android/forwarder.py
Normal file
476
tools/adb/systrace/catapult/devil/devil/android/forwarder.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
122
tools/adb/systrace/catapult/devil/devil/android/md5sum.py
Normal file
122
tools/adb/systrace/catapult/devil/devil/android/md5sum.py
Normal 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)
|
||||
|
||||
237
tools/adb/systrace/catapult/devil/devil/android/md5sum_test.py
Normal file
237
tools/adb/systrace/catapult/devil/devil/android/md5sum_test.py
Normal 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)
|
||||
|
||||
@@ -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.
|
||||
16
tools/adb/systrace/catapult/devil/devil/android/ndk/abis.py
Normal file
16
tools/adb/systrace/catapult/devil/devil/android/ndk/abis.py
Normal 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'
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
@@ -0,0 +1,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)
|
||||
@@ -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"')
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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]))
|
||||
@@ -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
|
||||
178
tools/adb/systrace/catapult/devil/devil/android/ports.py
Normal file
178
tools/adb/systrace/catapult/devil/devil/android/ports.py
Normal 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')
|
||||
@@ -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.
|
||||
43
tools/adb/systrace/catapult/devil/devil/android/sdk/aapt.py
Normal file
43
tools/adb/systrace/catapult/devil/devil/android/sdk/aapt.py
Normal 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()
|
||||
@@ -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())
|
||||
1003
tools/adb/systrace/catapult/devil/devil/android/sdk/adb_wrapper.py
Normal file
1003
tools/adb/systrace/catapult/devil/devil/android/sdk/adb_wrapper.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
122
tools/adb/systrace/catapult/devil/devil/android/sdk/fastboot.py
Normal file
122
tools/adb/systrace/catapult/devil/devil/android/sdk/fastboot.py
Normal 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))])
|
||||
@@ -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
|
||||
129
tools/adb/systrace/catapult/devil/devil/android/sdk/intent.py
Normal file
129
tools/adb/systrace/catapult/devil/devil/android/sdk/intent.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
Goodnight, moon.
|
||||
@@ -0,0 +1 @@
|
||||
Hello, world!
|
||||
@@ -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
|
||||
287
tools/adb/systrace/catapult/devil/devil/android/settings.py
Normal file
287
tools/adb/systrace/catapult/devil/devil/android/settings.py
Normal 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
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
@@ -0,0 +1,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())
|
||||
@@ -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:]))
|
||||
@@ -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:]))
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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:]))
|
||||
@@ -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:]))
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
@@ -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:]))
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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:]))
|
||||
@@ -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:]))
|
||||
@@ -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().
|
||||
"""
|
||||
@@ -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
|
||||
24
tools/adb/systrace/catapult/devil/devil/base_error.py
Normal file
24
tools/adb/systrace/catapult/devil/devil/base_error.py
Normal 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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
@@ -0,0 +1,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
|
||||
141
tools/adb/systrace/catapult/devil/devil/devil_dependencies.json
Normal file
141
tools/adb/systrace/catapult/devil/devil/devil_dependencies.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
tools/adb/systrace/catapult/devil/devil/devil_env.py
Normal file
194
tools/adb/systrace/catapult/devil/devil/devil_env.py
Normal 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()
|
||||
|
||||
63
tools/adb/systrace/catapult/devil/devil/devil_env_test.py
Normal file
63
tools/adb/systrace/catapult/devil/devil/devil_env_test.py
Normal 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)
|
||||
501
tools/adb/systrace/catapult/devil/devil/utils/cmd_helper.py
Normal file
501
tools/adb/systrace/catapult/devil/devil/utils/cmd_helper.py
Normal 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
Reference in New Issue
Block a user