aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil
diff options
context:
space:
mode:
authorChris Craik <ccraik@google.com>2017-04-03 15:54:29 -0700
committerChris Craik <ccraik@google.com>2017-04-03 16:02:14 -0700
commitc088b23ae5d8cab6878e43cc37b7b5d141f6c6d7 (patch)
treedf2eac86f23cb2dab056b1972c16a6222e005a89 /catapult/devil
parentb6ff195e4c30012fa6a7b3fe13a7938dc1c007ea (diff)
parent9ede47ea07acc34a4fe73d2ef5ba128fe1511483 (diff)
downloadchromium-trace-c088b23ae5d8cab6878e43cc37b7b5d141f6c6d7.tar.gz
resolve merge conflicts of 9ede47ea to oc-dev-plus-aosp
Test: external/chromium-trace/systrace.py Change-Id: I06f10b563d774ebcd6ebee75eb3622d0d71795c2
Diffstat (limited to 'catapult/devil')
-rw-r--r--catapult/devil/README.md37
-rwxr-xr-xcatapult/devil/bin/generate_md_docs45
-rw-r--r--catapult/devil/devil/README.md17
-rw-r--r--catapult/devil/devil/android/apk_helper.py54
-rwxr-xr-xcatapult/devil/devil/android/apk_helper_test.py169
-rw-r--r--catapult/devil/devil/android/constants/chrome.py18
-rw-r--r--catapult/devil/devil/android/device_utils.py202
-rwxr-xr-xcatapult/devil/devil/android/device_utils_devicetest.py60
-rwxr-xr-xcatapult/devil/devil/android/device_utils_test.py404
-rw-r--r--catapult/devil/devil/android/flag_changer.py77
-rw-r--r--catapult/devil/devil/android/flag_changer_devicetest.py13
-rwxr-xr-xcatapult/devil/devil/android/flag_changer_test.py17
-rw-r--r--catapult/devil/devil/android/forwarder.py18
-rw-r--r--catapult/devil/devil/android/install_commands.py4
-rw-r--r--catapult/devil/devil/android/logcat_monitor.py15
-rw-r--r--catapult/devil/devil/android/md5sum.py6
-rw-r--r--catapult/devil/devil/android/perf/cache_control.py3
-rw-r--r--catapult/devil/devil/android/perf/perf_control.py2
-rw-r--r--catapult/devil/devil/android/perf/surface_stats_collector.py15
-rw-r--r--catapult/devil/devil/android/perf/thermal_throttle.py3
-rw-r--r--catapult/devil/devil/android/sdk/adb_wrapper.py32
-rwxr-xr-xcatapult/devil/devil/android/sdk/adb_wrapper_test.py59
-rw-r--r--catapult/devil/devil/android/sdk/keyevent.py2
-rw-r--r--catapult/devil/devil/android/sdk/shared_prefs.py27
-rwxr-xr-xcatapult/devil/devil/android/sdk/shared_prefs_test.py5
-rw-r--r--catapult/devil/devil/android/sdk/version_codes.py1
-rw-r--r--catapult/devil/devil/android/settings.py40
-rwxr-xr-xcatapult/devil/devil/android/tools/device_monitor.py22
-rwxr-xr-xcatapult/devil/devil/android/tools/device_monitor_test.py30
-rwxr-xr-xcatapult/devil/devil/android/tools/provision_devices.py99
-rwxr-xr-xcatapult/devil/devil/android/tools/video_recorder.py2
-rw-r--r--catapult/devil/devil/utils/cmd_helper.py91
-rwxr-xr-xcatapult/devil/devil/utils/cmd_helper_test.py179
-rwxr-xr-xcatapult/devil/devil/utils/markdown.py264
-rwxr-xr-xcatapult/devil/devil/utils/markdown_test.py121
-rw-r--r--catapult/devil/docs/adb_wrapper.md388
-rw-r--r--catapult/devil/docs/device_utils.md1041
-rw-r--r--catapult/devil/docs/markdown.md139
38 files changed, 3324 insertions, 397 deletions
diff --git a/catapult/devil/README.md b/catapult/devil/README.md
new file mode 100644
index 00000000..852ac378
--- /dev/null
+++ b/catapult/devil/README.md
@@ -0,0 +1,37 @@
+<!-- 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.
+-->
+## devil
+
+😈
+
+devil is a library used by the Chromium developers to interact with Android
+devices. It currently supports SDK level 16 and above.
+
+## Interfaces
+
+devil provides python APIs:
+ - [`devil.android.adb_wrapper`](docs/adb_wrapper.md) provides a thin wrapper
+ around the adb binary. Most functions and methods have direct analogues on
+ the adb command-line.
+ - [`devil.android.device_utils`](docs/device_utils.md) provides higher-level
+ functionality built on top of `adb_wrapper`. **This is the primary
+ mechanism through which chromium's scripts interact with devices.**
+
+## Utilities
+
+devil also provides command-line utilities:
+ - [`devil/utils/markdown.py`](docs/markdown.md) generated markdown
+ documentation for python modules.
+
+## Constraints and Caveats
+
+devil is used with python 2.7. Its compatibility with python 3 has not been
+tested, and neither achieving nor maintaining said compatibility is currently
+a priority.
+
+## Contributing
+
+Please see the [contributor's guide](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md).
+
diff --git a/catapult/devil/bin/generate_md_docs b/catapult/devil/bin/generate_md_docs
new file mode 100755
index 00000000..634e14a5
--- /dev/null
+++ b/catapult/devil/bin/generate_md_docs
@@ -0,0 +1,45 @@
+#!/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
+
+_DEVIL_PATH = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), '..'))
+_DEVIL_URL = (
+ 'https://github.com/catapult-project/catapult/blob/master/devil/')
+
+sys.path.append(_DEVIL_PATH)
+from devil.utils import cmd_helper
+
+_FILES_TO_DOC = {
+ 'devil/android/sdk/adb_wrapper.py': 'docs/adb_wrapper.md',
+ 'devil/android/device_utils.py': 'docs/device_utils.md',
+ 'devil/utils/markdown.py': 'docs/markdown.md',
+}
+
+_MARKDOWN_SCRIPT = os.path.join(_DEVIL_PATH, 'devil', 'utils', 'markdown.py')
+
+def main():
+ failed = False
+ for k, v in _FILES_TO_DOC.iteritems():
+ module_path = os.path.join(_DEVIL_PATH, k)
+ module_link = _DEVIL_URL + k
+ doc_path = os.path.join(_DEVIL_PATH, v)
+
+ status, stdout = cmd_helper.GetCmdStatusAndOutput(
+ [sys.executable, _MARKDOWN_SCRIPT, module_path,
+ '--module-link', module_link])
+ if status:
+ logging.error('Failed to update doc for %s' % module_path)
+ failed = True
+ else:
+ with open(doc_path, 'w') as doc_file:
+ doc_file.write(stdout)
+
+ return 1 if failed else 0
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/catapult/devil/devil/README.md b/catapult/devil/devil/README.md
deleted file mode 100644
index b3eb5d0f..00000000
--- a/catapult/devil/devil/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-<!-- 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.
--->
-devil
-=====
-
-devil is a library used by the Chromium developers to interact with Android
-devices. It currently supports SDK level 16 and above.
-
-😈
-
-Contributing
-============
-
-Please see the [contributor's guide](https://github.com/catapult-project/catapult/blob/master/CONTRIBUTING.md).
-
diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py
index 61eeda06..1a9b8c55 100644
--- a/catapult/devil/devil/android/apk_helper.py
+++ b/catapult/devil/devil/android/apk_helper.py
@@ -4,8 +4,10 @@
"""Module containing utilities for apk packages."""
+import itertools
import re
+from devil import base_error
from devil.android.sdk import aapt
@@ -54,16 +56,22 @@ def _ParseManifestFromApk(apk_path):
m = _MANIFEST_ELEMENT_RE.match(line[len(indent) * indent_depth:])
if m:
- if not m.group(1) in node:
- node[m.group(1)] = {}
- node_stack += [node[m.group(1)]]
+ 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:
- if not m.group(1) in node:
- node[m.group(1)] = []
- node[m.group(1)].append(m.group(2) or m.group(3))
+ manifest_key = m.group(1)
+ if manifest_key in node:
+ raise base_error.BaseError(
+ "A single attribute should have one key and one value")
+ else:
+ node[manifest_key] = m.group(2) or m.group(3)
continue
return parsed_manifest
@@ -84,8 +92,8 @@ class ApkHelper(object):
manifest_info = self._GetManifest()
try:
activity = (
- manifest_info['manifest']['application']['activity']
- ['android:name'][0])
+ manifest_info['manifest'][0]['application'][0]['activity'][0]
+ ['android:name'])
except KeyError:
return None
if '.' not in activity:
@@ -97,24 +105,34 @@ class ApkHelper(object):
def GetInstrumentationName(
self, default='android.test.InstrumentationTestRunner'):
"""Returns the name of the Instrumentation in the apk."""
- manifest_info = self._GetManifest()
+ 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 all_instrumentations[0]['android:name']
+
+ def GetAllInstrumentations(
+ self, default='android.test.InstrumentationTestRunner'):
+ """Returns a list of all Instrumentations in the apk."""
try:
- return manifest_info['manifest']['instrumentation']['android:name'][0]
+ return self._GetManifest()['manifest'][0]['instrumentation']
except KeyError:
- return default
+ return [{'android:name': default}]
def GetPackageName(self):
"""Returns the package name of the apk."""
manifest_info = self._GetManifest()
try:
- return manifest_info['manifest']['package'][0]
+ 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 manifest_info['manifest']['uses-permission']['android:name']
+ return [p['android:name'] for
+ p in manifest_info['manifest'][0]['uses-permission']]
except KeyError:
return []
@@ -122,7 +140,7 @@ class ApkHelper(object):
"""Returns the name of the split of the apk."""
manifest_info = self._GetManifest()
try:
- return manifest_info['manifest']['split'][0]
+ return manifest_info['manifest'][0]['split']
except KeyError:
return None
@@ -130,8 +148,12 @@ class ApkHelper(object):
"""Returns whether any services exist that use isolatedProcess=true."""
manifest_info = self._GetManifest()
try:
- services = manifest_info['manifest']['application']['service']
- return any(int(v, 0) for v in services['android:isolatedProcess'])
+ applications = manifest_info['manifest'][0].get('application', [])
+ services = itertools.chain(
+ *(application.get('service', []) for application in applications))
+ return any(
+ int(s.get('android:isolatedProcess', '0'), 0)
+ for s in services)
except KeyError:
return False
diff --git a/catapult/devil/devil/android/apk_helper_test.py b/catapult/devil/devil/android/apk_helper_test.py
new file mode 100755
index 00000000..f7d077dd
--- /dev/null
+++ b/catapult/devil/devil/android/apk_helper_test.py
@@ -0,0 +1,169 @@
+#! /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.
+
+from devil import base_error
+from devil import devil_env
+from devil.android import apk_helper
+from devil.utils import mock_calls
+
+with devil_env.SysPath(devil_env.PYMOCK_PATH):
+ import mock # pylint: disable=import-error
+
+
+_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
+ E: manifest (line=1)
+ A: package="org.chromium.abc" (Raw: "org.chromium.abc")
+ A: split="random_split" (Raw: "random_split")
+ 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: 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")
+"""
+
+
+def _MockAaptDump(manifest_dump):
+ return mock.patch(
+ 'devil.android.sdk.aapt.Dump',
+ mock.Mock(side_effect=None, return_value=manifest_dump.split('\n')))
+
+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.ActivityName')
+
+ 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())
+
diff --git a/catapult/devil/devil/android/constants/chrome.py b/catapult/devil/devil/android/constants/chrome.py
index 006764f2..dca04bdc 100644
--- a/catapult/devil/devil/android/constants/chrome.py
+++ b/catapult/devil/devil/android/constants/chrome.py
@@ -12,46 +12,46 @@ PACKAGE_INFO = {
'chrome_document': PackageInfo(
'com.google.android.apps.chrome.document',
'com.google.android.apps.chrome.document.ChromeLauncherActivity',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chrome': PackageInfo(
'com.google.android.apps.chrome',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chrome_beta': PackageInfo(
'com.chrome.beta',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chrome_stable': PackageInfo(
'com.android.chrome',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chrome_dev': PackageInfo(
'com.chrome.dev',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chrome_canary': PackageInfo(
'com.chrome.canary',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chrome_work': PackageInfo(
'com.chrome.work',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'chromium': PackageInfo(
'org.chromium.chrome',
'com.google.android.apps.chrome.Main',
- '/data/local/chrome-command-line',
+ 'chrome-command-line',
'chrome_devtools_remote'),
'content_shell': PackageInfo(
'org.chromium.content_shell_apk',
'.ContentShellActivity',
- '/data/local/tmp/content-shell-command-line',
+ 'content-shell-command-line',
'content_shell_devtools_remote'),
}
diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py
index 3d8e2034..7ba1b514 100644
--- a/catapult/devil/devil/android/device_utils.py
+++ b/catapult/devil/devil/android/device_utils.py
@@ -76,13 +76,22 @@ _PERMISSIONS_BLACKLIST = [
'android.permission.ACCESS_LOCATION_EXTRA_COMMANDS',
'android.permission.ACCESS_MOCK_LOCATION',
'android.permission.ACCESS_NETWORK_STATE',
+ 'android.permission.ACCESS_NOTIFICATION_POLICY',
'android.permission.ACCESS_WIFI_STATE',
'android.permission.AUTHENTICATE_ACCOUNTS',
'android.permission.BLUETOOTH',
'android.permission.BLUETOOTH_ADMIN',
+ 'android.permission.BROADCAST_STICKY',
+ 'android.permission.CHANGE_NETWORK_STATE',
+ 'android.permission.CHANGE_WIFI_MULTICAST_STATE',
+ 'android.permission.CHANGE_WIFI_STATE',
'android.permission.DISABLE_KEYGUARD',
'android.permission.DOWNLOAD_WITHOUT_NOTIFICATION',
+ 'android.permission.EXPAND_STATUS_BAR',
+ 'android.permission.GET_PACKAGE_SIZE',
+ 'android.permission.INSTALL_SHORTCUT',
'android.permission.INTERNET',
+ 'android.permission.KILL_BACKGROUND_PROCESSES',
'android.permission.MANAGE_ACCOUNTS',
'android.permission.MODIFY_AUDIO_SETTINGS',
'android.permission.NFC',
@@ -90,8 +99,16 @@ _PERMISSIONS_BLACKLIST = [
'android.permission.READ_SYNC_STATS',
'android.permission.RECEIVE_BOOT_COMPLETED',
'android.permission.RECORD_VIDEO',
+ 'android.permission.REORDER_TASKS',
+ 'android.permission.REQUEST_INSTALL_PACKAGES',
'android.permission.RUN_INSTRUMENTATION',
+ 'android.permission.SET_ALARM',
+ 'android.permission.SET_TIME_ZONE',
+ 'android.permission.SET_WALLPAPER',
+ 'android.permission.SET_WALLPAPER_HINTS',
+ 'android.permission.TRANSMIT_IR',
'android.permission.USE_CREDENTIALS',
+ 'android.permission.USE_FINGERPRINT',
'android.permission.VIBRATE',
'android.permission.WAKE_LOCK',
'android.permission.WRITE_SYNC_SETTINGS',
@@ -150,6 +167,17 @@ _FILE_MODE_SPECIAL = [
('s', stat.S_ISGID),
('t', stat.S_ISVTX),
]
+_SELINUX_MODE = {
+ 'enforcing': True,
+ 'permissive': False,
+ 'disabled': None
+}
+# Some devices require different logic for checking if root is necessary
+_SPECIAL_ROOT_DEVICE_LIST = [
+ 'marlin',
+ 'sailfish',
+]
+
@decorators.WithExplicitTimeoutAndRetries(
_DEFAULT_TIMEOUT, _DEFAULT_RETRIES)
@@ -367,6 +395,8 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
try:
+ if self.product_name in _SPECIAL_ROOT_DEVICE_LIST:
+ return self.GetProp('service.adb.root') == '1'
self.RunShellCommand(['ls', '/root'], check_return=True)
return True
except device_errors.AdbCommandFailedError:
@@ -390,9 +420,14 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
if 'needs_su' not in self._cache:
+ cmd = '%s && ! ls /root' % self._Su('ls /root')
+ if self.product_name in _SPECIAL_ROOT_DEVICE_LIST:
+ if self.HasRoot():
+ self._cache['needs_su'] = False
+ return False
+ cmd = 'which which && which su'
try:
- self.RunShellCommand(
- '%s && ! ls /root' % self._Su('ls /root'), check_return=True,
+ self.RunShellCommand(cmd, shell=True, check_return=True,
timeout=self._default_timeout if timeout is DEFAULT else timeout,
retries=self._default_retries if retries is DEFAULT else retries)
self._cache['needs_su'] = True
@@ -400,6 +435,7 @@ class DeviceUtils(object):
self._cache['needs_su'] = False
return self._cache['needs_su']
+
def _Su(self, command):
if self.build_version_sdk >= version_codes.MARSHMALLOW:
return 'su 0 %s' % command
@@ -799,24 +835,27 @@ class DeviceUtils(object):
device_serial=self.serial)
@decorators.WithTimeoutAndRetriesFromInstance()
- def RunShellCommand(self, cmd, check_return=False, cwd=None, env=None,
- run_as=None, as_root=False, single_line=False,
+ def RunShellCommand(self, 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):
"""Run an ADB shell command.
- The command to run |cmd| should be a sequence of program arguments or else
- a single string.
+ The command to run |cmd| should be a sequence of program arguments
+ (preferred) or a single string with a shell script to run.
When |cmd| is a sequence, it is assumed to contain the name of the command
to run followed by its arguments. In this case, arguments are passed to the
- command exactly as given, without any further processing by the shell. This
- allows to easily pass arguments containing spaces or special characters
- without having to worry about getting quoting right. Whenever possible, it
- is recomended to pass |cmd| as a sequence.
+ command exactly as given, preventing any further processing by the shell.
+ This allows callers to easily pass arguments with spaces or special
+ characters without having to worry about quoting rules. Whenever possible,
+ it is recomended to pass |cmd| as a sequence.
- When |cmd| is given as a string, it will be interpreted and run by the
- shell on the device.
+ When |cmd| is passed as a single string, |shell| should be set to True.
+ The command will be interpreted and run by the shell on the device,
+ allowing the use of shell features such as pipes, wildcards, or variables.
+ Failing to set shell=True will issue a warning, but this will be changed
+ to a hard failure in the future (see: catapult:#3242).
This behaviour is consistent with that of command runners in cmd_helper as
well as Python's own subprocess.Popen.
@@ -825,8 +864,9 @@ class DeviceUtils(object):
have switched to the new behaviour.
Args:
- cmd: A string with the full command to run on the device, or a sequence
- containing the command and its arguments.
+ cmd: A sequence containing the command to run and its arguments, or a
+ string with a shell script to run (should also set shell=True).
+ shell: A boolean indicating whether shell features may be used in |cmd|.
check_return: A boolean indicating whether or not the return code should
be checked.
cwd: The device directory in which the command should be run.
@@ -906,7 +946,13 @@ class DeviceUtils(object):
else:
raise
- if not isinstance(cmd, basestring):
+ if isinstance(cmd, basestring):
+ if not shell:
+ logging.warning(
+ 'The command to run should preferably be passed as a sequence of'
+ ' args. If shell features are needed (pipes, wildcards, variables)'
+ ' clients should explicitly set shell=True.')
+ else:
cmd = ' '.join(cmd_helper.SingleQuote(s) for s in cmd)
if env:
env = ' '.join(env_quote(k, v) for k, v in env.iteritems())
@@ -941,7 +987,7 @@ class DeviceUtils(object):
PIPESTATUS_LEADER = 'PIPESTATUS: '
script += '; echo "%s${PIPESTATUS[@]}"' % PIPESTATUS_LEADER
- kwargs['check_return'] = True
+ kwargs.update(shell=True, check_return=True)
output = self.RunShellCommand(script, **kwargs)
pipestatus_line = output[-1]
@@ -1073,7 +1119,7 @@ class DeviceUtils(object):
package = component.split('/')[0]
shell_snippet = 'p=%s;%s' % (package,
cmd_helper.ShrinkToSnippet(cmd, 'p', package))
- return self.RunShellCommand(shell_snippet, check_return=True,
+ return self.RunShellCommand(shell_snippet, shell=True, check_return=True,
large_output=True)
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -1145,7 +1191,7 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
cmd = 'p=%s;if [[ "$(ps)" = *$p* ]]; then am force-stop $p; fi'
- self.RunShellCommand(cmd % package, check_return=True)
+ self.RunShellCommand(cmd % package, shell=True, check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
def ClearApplicationState(
@@ -1451,7 +1497,7 @@ class DeviceUtils(object):
quoted_dirs = ' '.join(cmd_helper.SingleQuote(d) for d in dirs)
self.RunShellCommand(
'unzip %s&&chmod -R 777 %s' % (device_temp.name, quoted_dirs),
- as_root=True,
+ shell=True, as_root=True,
env={'PATH': '%s:$PATH' % install_commands.BIN_DIR},
check_return=True)
finally:
@@ -1496,8 +1542,11 @@ class DeviceUtils(object):
paths = device_paths
if isinstance(paths, basestring):
paths = (paths,)
- condition = ' -a '.join('-e %s' % cmd_helper.SingleQuote(p) for p in paths)
- cmd = 'test %s' % condition
+ if not paths:
+ return True
+ cmd = ['test', '-e', paths[0]]
+ for p in paths[1:]:
+ cmd.extend(['-a', '-e', p])
try:
self.RunShellCommand(cmd, as_root=as_root, check_return=True,
timeout=timeout, retries=retries)
@@ -1603,7 +1652,7 @@ class DeviceUtils(object):
cmd = 'SRC=%s DEST=%s;cp "$SRC" "$DEST" && chmod 666 "$DEST"' % (
cmd_helper.SingleQuote(device_path),
cmd_helper.SingleQuote(device_temp.name))
- self.RunShellCommand(cmd, as_root=True, check_return=True)
+ self.RunShellCommand(cmd, shell=True, as_root=True, check_return=True)
return self._ReadFileWithPull(device_temp.name)
else:
return self._ReadFileWithPull(device_path)
@@ -1641,7 +1690,7 @@ class DeviceUtils(object):
# a shell command rather than pushing a file.
cmd = 'echo -n %s > %s' % (cmd_helper.SingleQuote(contents),
cmd_helper.SingleQuote(device_path))
- self.RunShellCommand(cmd, as_root=as_root, check_return=True)
+ self.RunShellCommand(cmd, shell=True, as_root=as_root, check_return=True)
elif as_root and self.NeedsSU():
# Adb does not allow to "push with su", so we first push to a temp file
# on a safe location, and then copy it to the desired location with su.
@@ -1993,7 +2042,8 @@ class DeviceUtils(object):
'echo "%s">$c &&' % token +
'getprop'
)
- output = self.RunShellCommand(cmd, check_return=True, large_output=True)
+ output = self.RunShellCommand(
+ cmd, shell=True, check_return=True, large_output=True)
# Error-checking for this existing is done in GetExternalStoragePath().
self._cache['external_storage'] = output[0]
self._cache['prev_token'] = output[1]
@@ -2091,13 +2141,14 @@ class DeviceUtils(object):
return self.GetProp('ro.product.cpu.abi', cache=True)
@decorators.WithTimeoutAndRetriesFromInstance()
- def GetPids(self, process_name, timeout=None, retries=None):
- """Returns the PIDs of processes with the given name.
+ def GetPids(self, process_name=None, timeout=None, retries=None):
+ """Returns the PIDs of processes containing the given name as substring.
Note that the |process_name| is often the package name.
Args:
process_name: A string containing the process name to get the PIDs for.
+ If missing returns PIDs for all processes.
timeout: timeout in seconds
retries: number of retries
@@ -2111,8 +2162,17 @@ class DeviceUtils(object):
"""
procs_pids = collections.defaultdict(list)
try:
- ps_output = self._RunPipedShellCommand(
- 'ps | grep -F %s' % cmd_helper.SingleQuote(process_name))
+ ps_cmd = 'ps'
+ # ps behavior was changed in Android above N, http://crbug.com/686716
+ if (self.build_version_sdk >= version_codes.NOUGAT_MR1
+ and self.build_id[0] > 'N'):
+ ps_cmd = 'ps -e'
+ if process_name:
+ ps_output = self._RunPipedShellCommand(
+ '%s | grep -F %s' % (ps_cmd, cmd_helper.SingleQuote(process_name)))
+ else:
+ ps_output = self.RunShellCommand(
+ ps_cmd.split(), check_return=True, large_output=True)
except device_errors.AdbShellCommandFailedError as e:
if e.status and isinstance(e.status, list) and not e.status[0]:
# If ps succeeded but grep failed, there were no processes with the
@@ -2121,16 +2181,91 @@ class DeviceUtils(object):
else:
raise
+ process_name = process_name or ''
for line in ps_output:
try:
ps_data = line.split()
- if process_name in ps_data[-1]:
- pid, process = ps_data[1], ps_data[-1]
+ pid, process = ps_data[1], ps_data[-1]
+ if process_name in process and pid != 'PID':
procs_pids[process].append(pid)
except IndexError:
pass
return procs_pids
+ def GetApplicationPids(self, process_name, at_most_one=False, **kwargs):
+ """Returns the PID or PIDs of a given process name.
+
+ Note that the |process_name|, often the package name, must match exactly.
+
+ Args:
+ process_name: A string containing the process name to get the PIDs for.
+ at_most_one: A boolean indicating that at most one PID is expected to
+ be found.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A list of the PIDs for the named process. If at_most_one=True returns
+ the single PID found or None otherwise.
+
+ Raises:
+ CommandFailedError if at_most_one=True and more than one PID is found
+ for the named process.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+ """
+ pids = self.GetPids(process_name, **kwargs).get(process_name, [])
+ if at_most_one:
+ if len(pids) > 1:
+ raise device_errors.CommandFailedError(
+ 'Expected a single process but found PIDs: %s.' % ', '.join(pids),
+ device_serial=str(self))
+ return pids[0] if pids else None
+ else:
+ return pids
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def GetEnforce(self, timeout=None, retries=None):
+ """Get the current mode of SELinux.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True (enforcing), False (permissive), or None (disabled).
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+ """
+ output = self.RunShellCommand(
+ ['getenforce'], check_return=True, single_line=True).lower()
+ if output not in _SELINUX_MODE:
+ raise device_errors.CommandFailedError(
+ 'Unexpected getenforce output: %s' % output)
+ return _SELINUX_MODE[output]
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
+ def SetEnforce(self, enabled, timeout=None, retries=None):
+ """Modify the mode SELinux is running in.
+
+ Args:
+ enabled: a boolean indicating whether to put SELinux in encorcing mode
+ (if True), or permissive mode (otherwise).
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+ """
+ self.RunShellCommand(
+ ['setenforce', '1' if int(enabled) else '0'], as_root=True,
+ check_return=True)
+
@decorators.WithTimeoutAndRetriesFromInstance()
def TakeScreenshot(self, host_path=None, timeout=None, retries=None):
"""Takes a screenshot of the device.
@@ -2440,7 +2575,8 @@ class DeviceUtils(object):
logger.info('Restarting adbd on device.')
with device_temp_file.DeviceTempFile(self.adb, suffix='.sh') as script:
self.WriteFile(script.name, _RESTART_ADBD_SCRIPT)
- self.RunShellCommand(['source', script.name], as_root=True)
+ self.RunShellCommand(
+ ['source', script.name], check_return=True, as_root=True)
self.adb.WaitForDevice()
@decorators.WithTimeoutAndRetriesFromInstance()
@@ -2456,7 +2592,7 @@ class DeviceUtils(object):
permissions.append('android.permission.READ_EXTERNAL_STORAGE')
cmd = '&&'.join('pm grant %s %s' % (package, p) for p in permissions)
if cmd:
- output = self.RunShellCommand(cmd, check_return=True)
+ output = self.RunShellCommand(cmd, shell=True, check_return=True)
if output:
logger.warning('Possible problem when granting permissions. Blacklist '
'may need to be updated.')
@@ -2509,5 +2645,5 @@ class DeviceUtils(object):
if screen_test():
logger.info('Screen already in expected state.')
return
- self.RunShellCommand('input keyevent 26')
+ self.SendKeyEvent(keyevent.KEYCODE_POWER)
timeout_retry.WaitFor(screen_test, wait_period=1)
diff --git a/catapult/devil/devil/android/device_utils_devicetest.py b/catapult/devil/devil/android/device_utils_devicetest.py
index 54ab7dbf..e69cc909 100755
--- a/catapult/devil/devil/android/device_utils_devicetest.py
+++ b/catapult/devil/devil/android/device_utils_devicetest.py
@@ -9,6 +9,7 @@ The test will invoke real devices
"""
import os
+import posixpath
import sys
import tempfile
import unittest
@@ -87,12 +88,12 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
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],
- single_line=True)
+ 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.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+ self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testPushChangedFiles_singleFileChange(self):
(host_file_path, file_name) = self._MakeTempFile(_OLD_CONTENTS)
@@ -102,12 +103,12 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
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],
- single_line=True)
+ 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.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+ self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testDeleteFiles(self):
host_tmp_dir = tempfile.mkdtemp()
@@ -120,11 +121,11 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
cmd_helper.RunCmd(['rm', host_file_path])
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
- result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True)
- self.assertEqual('', result)
+ filenames = self.device.ListDirectory(_DEVICE_DIR)
+ self.assertEqual([], filenames)
cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
- self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+ self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testPushAndDeleteFiles_noSubDir(self):
host_tmp_dir = tempfile.mkdtemp()
@@ -144,14 +145,15 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
- result = self.device.RunShellCommand(['cat', device_file_path1],
- single_line=True)
+ result = self.device.RunShellCommand(
+ ['cat', device_file_path1], check_return=True, single_line=True)
self.assertEqual(_NEW_CONTENTS, result)
- result = self.device.RunShellCommand(['ls', _DEVICE_DIR], single_line=True)
- self.assertEqual(file_name1, result)
- self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
+ 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()
@@ -187,31 +189,31 @@ class DeviceUtilsPushDeleteFilesTest(device_test_case.DeviceTestCase):
self.device.PushChangedFiles([(host_tmp_dir, _DEVICE_DIR)],
delete_device_stale=True)
- result = self.device.RunShellCommand(['cat', device_file_path1],
- single_line=True)
+ result = self.device.RunShellCommand(
+ ['cat', device_file_path1], check_return=True, single_line=True)
self.assertEqual(_NEW_CONTENTS, result)
- result = self.device.RunShellCommand(['ls', _DEVICE_DIR])
- self.assertIn(file_name1, result)
- self.assertIn(_SUB_DIR1, result)
- self.assertIn(_SUB_DIR, result)
- self.assertEqual(3, len(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],
- single_line=True)
+ result = self.device.RunShellCommand(
+ ['cat', device_file_path3], check_return=True, single_line=True)
self.assertEqual(_OLD_CONTENTS, result)
- result = self.device.RunShellCommand(["ls", "%s/%s/%s"
- % (_DEVICE_DIR, _SUB_DIR, _SUB_DIR2)],
- single_line=True)
- self.assertEqual('', result)
+ filenames = self.device.ListDirectory(
+ posixpath.join(_DEVICE_DIR, _SUB_DIR, _SUB_DIR2))
+ self.assertEqual([], filenames)
- self.device.RunShellCommand(['rm', '-rf', _DEVICE_DIR])
cmd_helper.RunCmd(['rm', '-rf', host_tmp_dir])
+ self.device.RemovePath(_DEVICE_DIR, recursive=True, force=True)
def testRestartAdbd(self):
def get_adbd_pid():
- ps_output = self.device.RunShellCommand(['ps'])
+ # TODO(catapult:#3215): Migrate to device.GetPids().
+ ps_output = self.device.RunShellCommand(['ps'], check_return=True)
for ps_line in ps_output:
if 'adbd' in ps_line:
return ps_line.split()[1]
diff --git a/catapult/devil/devil/android/device_utils_test.py b/catapult/devil/devil/android/device_utils_test.py
index a77d1202..24902096 100755
--- a/catapult/devil/devil/android/device_utils_test.py
+++ b/catapult/devil/devil/android/device_utils_test.py
@@ -22,6 +22,7 @@ from devil.android import device_signal
from devil.android import device_utils
from devil.android.sdk import adb_wrapper
from devil.android.sdk import intent
+from devil.android.sdk import keyevent
from devil.android.sdk import version_codes
from devil.utils import cmd_helper
from devil.utils import mock_calls
@@ -194,7 +195,8 @@ class DeviceUtilsTest(mock_calls.TestCase):
props = props or []
ret = [sdcard, 'TOKEN'] + props
return (self.call.device.RunShellCommand(
- AnyStringWith('getprop'), check_return=True, large_output=True), ret)
+ AnyStringWith('getprop'),
+ shell=True, check_return=True, large_output=True), ret)
class DeviceUtilsEqTest(DeviceUtilsTest):
@@ -282,11 +284,30 @@ class DeviceUtilsIsOnlineTest(DeviceUtilsTest):
class DeviceUtilsHasRootTest(DeviceUtilsTest):
def testHasRoot_true(self):
- with self.assertCall(self.call.adb.Shell('ls /root'), 'foo\n'):
+ with self.patch_call(self.call.device.product_name,
+ return_value='notasailfish'), (
+ self.assertCall(self.call.adb.Shell('ls /root'), 'foo\n')):
+ self.assertTrue(self.device.HasRoot())
+
+ def testhasRootSpecial_true(self):
+ with self.patch_call(self.call.device.product_name,
+ return_value='sailfish'), (
+ self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+ '1\n')):
self.assertTrue(self.device.HasRoot())
def testHasRoot_false(self):
- with self.assertCall(self.call.adb.Shell('ls /root'), self.ShellError()):
+ with self.patch_call(self.call.device.product_name,
+ return_value='notasailfish'), (
+ self.assertCall(self.call.adb.Shell('ls /root'),
+ self.ShellError())):
+ self.assertFalse(self.device.HasRoot())
+
+ def testHasRootSpecial_false(self):
+ with self.patch_call(self.call.device.product_name,
+ return_value='sailfish'), (
+ self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+ '\n')):
self.assertFalse(self.device.HasRoot())
@@ -849,38 +870,47 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_commandAsList(self):
with self.assertCall(self.call.adb.Shell('pm list packages'), ''):
- self.device.RunShellCommand(['pm', 'list', 'packages'])
+ self.device.RunShellCommand(
+ ['pm', 'list', 'packages'], check_return=True)
def testRunShellCommand_commandAsListQuoted(self):
with self.assertCall(self.call.adb.Shell("echo 'hello world' '$10'"), ''):
- self.device.RunShellCommand(['echo', 'hello world', '$10'])
+ self.device.RunShellCommand(
+ ['echo', 'hello world', '$10'], check_return=True)
def testRunShellCommand_commandAsString(self):
with self.assertCall(self.call.adb.Shell('echo "$VAR"'), ''):
- self.device.RunShellCommand('echo "$VAR"')
+ self.device.RunShellCommand(
+ 'echo "$VAR"', shell=True, check_return=True)
def testNewRunShellImpl_withEnv(self):
with self.assertCall(
self.call.adb.Shell('VAR=some_string echo "$VAR"'), ''):
- self.device.RunShellCommand('echo "$VAR"', env={'VAR': 'some_string'})
+ self.device.RunShellCommand(
+ 'echo "$VAR"', shell=True, check_return=True,
+ env={'VAR': 'some_string'})
def testNewRunShellImpl_withEnvQuoted(self):
with self.assertCall(
self.call.adb.Shell('PATH="$PATH:/other/path" run_this'), ''):
- self.device.RunShellCommand('run_this', env={'PATH': '$PATH:/other/path'})
+ self.device.RunShellCommand(
+ ['run_this'], check_return=True, env={'PATH': '$PATH:/other/path'})
def testNewRunShellImpl_withEnv_failure(self):
with self.assertRaises(KeyError):
- self.device.RunShellCommand('some_cmd', env={'INVALID NAME': 'value'})
+ self.device.RunShellCommand(
+ ['some_cmd'], check_return=True, env={'INVALID NAME': 'value'})
def testNewRunShellImpl_withCwd(self):
with self.assertCall(self.call.adb.Shell('cd /some/test/path && ls'), ''):
- self.device.RunShellCommand('ls', cwd='/some/test/path')
+ self.device.RunShellCommand(
+ ['ls'], check_return=True, cwd='/some/test/path')
def testNewRunShellImpl_withCwdQuoted(self):
with self.assertCall(
self.call.adb.Shell("cd '/some test/path with/spaces' && ls"), ''):
- self.device.RunShellCommand('ls', cwd='/some test/path with/spaces')
+ self.device.RunShellCommand(
+ ['ls'], check_return=True, cwd='/some test/path with/spaces')
def testRunShellCommand_withHugeCmd(self):
payload = 'hi! ' * 1024
@@ -890,8 +920,9 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
(self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
- self.assertEquals([payload],
- self.device.RunShellCommand(['echo', payload]))
+ self.assertEquals(
+ [payload],
+ self.device.RunShellCommand(['echo', payload], check_return=True))
def testRunShellCommand_withHugeCmdAndSu(self):
payload = 'hi! ' * 1024
@@ -906,7 +937,8 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
(self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
self.assertEquals(
[payload],
- self.device.RunShellCommand(['echo', payload], as_root=True))
+ self.device.RunShellCommand(
+ ['echo', payload], check_return=True, as_root=True))
def testRunShellCommand_withSu(self):
expected_cmd_without_su = "sh -c 'setprop service.adb.root 0'"
@@ -915,7 +947,9 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
(self.call.device.NeedsSU(), True),
(self.call.device._Su(expected_cmd_without_su), expected_cmd),
(self.call.adb.Shell(expected_cmd), '')):
- self.device.RunShellCommand('setprop service.adb.root 0', as_root=True)
+ self.device.RunShellCommand(
+ ['setprop', 'service.adb.root', '0'],
+ check_return=True, as_root=True)
def testRunShellCommand_withRunAs(self):
expected_cmd_without_run_as = "sh -c 'mkdir -p files'"
@@ -924,7 +958,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
with self.assertCall(self.call.adb.Shell(expected_cmd), ''):
self.device.RunShellCommand(
['mkdir', '-p', 'files'],
- run_as='org.devil.test_package')
+ check_return=True, run_as='org.devil.test_package')
def testRunShellCommand_withRunAsAndSu(self):
expected_cmd_with_nothing = "sh -c 'mkdir -p files'"
@@ -939,72 +973,86 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
(self.call.adb.Shell(expected_cmd), '')):
self.device.RunShellCommand(
['mkdir', '-p', 'files'],
- run_as='org.devil.test_package',
+ check_return=True, run_as='org.devil.test_package',
as_root=True)
def testRunShellCommand_manyLines(self):
cmd = 'ls /some/path'
with self.assertCall(self.call.adb.Shell(cmd), 'file1\nfile2\nfile3\n'):
- self.assertEquals(['file1', 'file2', 'file3'],
- self.device.RunShellCommand(cmd))
+ self.assertEquals(
+ ['file1', 'file2', 'file3'],
+ self.device.RunShellCommand(cmd.split(), check_return=True))
def testRunShellCommand_manyLinesRawOutput(self):
cmd = 'ls /some/path'
with self.assertCall(self.call.adb.Shell(cmd), '\rfile1\nfile2\r\nfile3\n'):
- self.assertEquals('\rfile1\nfile2\r\nfile3\n',
- self.device.RunShellCommand(cmd, raw_output=True))
+ self.assertEquals(
+ '\rfile1\nfile2\r\nfile3\n',
+ self.device.RunShellCommand(
+ cmd.split(), check_return=True, raw_output=True))
def testRunShellCommand_singleLine_success(self):
cmd = 'echo $VALUE'
with self.assertCall(self.call.adb.Shell(cmd), 'some value\n'):
- self.assertEquals('some value',
- self.device.RunShellCommand(cmd, single_line=True))
+ self.assertEquals(
+ 'some value',
+ self.device.RunShellCommand(
+ cmd, shell=True, check_return=True, single_line=True))
def testRunShellCommand_singleLine_successEmptyLine(self):
cmd = 'echo $VALUE'
with self.assertCall(self.call.adb.Shell(cmd), '\n'):
- self.assertEquals('',
- self.device.RunShellCommand(cmd, single_line=True))
+ self.assertEquals(
+ '',
+ self.device.RunShellCommand(
+ cmd, shell=True, check_return=True, single_line=True))
def testRunShellCommand_singleLine_successWithoutEndLine(self):
cmd = 'echo -n $VALUE'
with self.assertCall(self.call.adb.Shell(cmd), 'some value'):
- self.assertEquals('some value',
- self.device.RunShellCommand(cmd, single_line=True))
+ self.assertEquals(
+ 'some value',
+ self.device.RunShellCommand(
+ cmd, shell=True, check_return=True, single_line=True))
def testRunShellCommand_singleLine_successNoOutput(self):
cmd = 'echo -n $VALUE'
with self.assertCall(self.call.adb.Shell(cmd), ''):
- self.assertEquals('',
- self.device.RunShellCommand(cmd, single_line=True))
+ self.assertEquals(
+ '',
+ self.device.RunShellCommand(
+ cmd, shell=True, check_return=True, single_line=True))
def testRunShellCommand_singleLine_failTooManyLines(self):
cmd = 'echo $VALUE'
with self.assertCall(self.call.adb.Shell(cmd),
'some value\nanother value\n'):
with self.assertRaises(device_errors.CommandFailedError):
- self.device.RunShellCommand(cmd, single_line=True)
+ self.device.RunShellCommand(
+ cmd, shell=True, check_return=True, single_line=True)
def testRunShellCommand_checkReturn_success(self):
cmd = 'echo $ANDROID_DATA'
output = '/data\n'
with self.assertCall(self.call.adb.Shell(cmd), output):
- self.assertEquals([output.rstrip()],
- self.device.RunShellCommand(cmd, check_return=True))
+ self.assertEquals(
+ [output.rstrip()],
+ self.device.RunShellCommand(cmd, shell=True, check_return=True))
def testRunShellCommand_checkReturn_failure(self):
cmd = 'ls /root'
output = 'opendir failed, Permission denied\n'
with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)):
with self.assertRaises(device_errors.AdbCommandFailedError):
- self.device.RunShellCommand(cmd, check_return=True)
+ self.device.RunShellCommand(cmd.split(), check_return=True)
def testRunShellCommand_checkReturn_disabled(self):
cmd = 'ls /root'
output = 'opendir failed, Permission denied\n'
with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)):
- self.assertEquals([output.rstrip()],
- self.device.RunShellCommand(cmd, check_return=False))
+ self.assertEquals(
+ [output.rstrip()],
+ self.device.RunShellCommand(cmd.split(), check_return=False))
def testRunShellCommand_largeOutput_enabled(self):
cmd = 'echo $VALUE'
@@ -1019,13 +1067,13 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
self.assertEquals(
['something'],
self.device.RunShellCommand(
- cmd, large_output=True, check_return=True))
+ cmd, shell=True, large_output=True, check_return=True))
def testRunShellCommand_largeOutput_disabledNoTrigger(self):
cmd = 'something'
with self.assertCall(self.call.adb.Shell(cmd), self.ShellError('')):
with self.assertRaises(device_errors.AdbCommandFailedError):
- self.device.RunShellCommand(cmd, check_return=True)
+ self.device.RunShellCommand([cmd], check_return=True)
def testRunShellCommand_largeOutput_disabledTrigger(self):
cmd = 'echo $VALUE'
@@ -1038,8 +1086,9 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
(self.call.adb.Shell(cmd_redirect)),
(self.call.device.ReadFile(mock.ANY, force_pull=True),
'something')):
- self.assertEquals(['something'],
- self.device.RunShellCommand(cmd, check_return=True))
+ self.assertEquals(
+ ['something'],
+ self.device.RunShellCommand(cmd, shell=True, check_return=True))
class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest):
@@ -1048,7 +1097,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest):
with self.assertCall(
self.call.device.RunShellCommand(
'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"',
- check_return=True),
+ shell=True, check_return=True),
['This line contains foo', 'PIPESTATUS: 0 0']):
self.assertEquals(['This line contains foo'],
self.device._RunPipedShellCommand('ps | grep foo'))
@@ -1057,7 +1106,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest):
with self.assertCall(
self.call.device.RunShellCommand(
'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"',
- check_return=True),
+ shell=True, check_return=True),
['PIPESTATUS: 1 0']):
with self.assertRaises(device_errors.AdbShellCommandFailedError) as ec:
self.device._RunPipedShellCommand('ps | grep foo')
@@ -1067,7 +1116,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest):
with self.assertCall(
self.call.device.RunShellCommand(
'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"',
- check_return=True),
+ shell=True, check_return=True),
['PIPESTATUS: 0 1']):
with self.assertRaises(device_errors.AdbShellCommandFailedError) as ec:
self.device._RunPipedShellCommand('ps | grep foo')
@@ -1077,7 +1126,7 @@ class DeviceUtilsRunPipedShellCommandTest(DeviceUtilsTest):
with self.assertCall(
self.call.device.RunShellCommand(
'ps | grep foo; echo "PIPESTATUS: ${PIPESTATUS[@]}"',
- check_return=True),
+ shell=True, check_return=True),
['foo.bar'] * 256 + ['foo.ba']):
with self.assertRaises(device_errors.AdbShellCommandFailedError) as ec:
self.device._RunPipedShellCommand('ps | grep foo')
@@ -1336,7 +1385,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest):
with self.assertCalls(
self.call.device.RunShellCommand(
'p=test.package;am instrument "$p"/.TestInstrumentation',
- check_return=True, large_output=True)):
+ shell=True, check_return=True, large_output=True)):
self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
finish=False, raw=False, extras=None)
@@ -1345,7 +1394,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.RunShellCommand(
'p=test.package;am instrument -w "$p"/.TestInstrumentation',
- check_return=True, large_output=True),
+ shell=True, check_return=True, large_output=True),
['OK (1 test)'])):
output = self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
@@ -1356,7 +1405,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest):
with self.assertCalls(
self.call.device.RunShellCommand(
'p=test.package;am instrument -r "$p"/.TestInstrumentation',
- check_return=True, large_output=True)):
+ shell=True, check_return=True, large_output=True)):
self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
finish=False, raw=True, extras=None)
@@ -1366,7 +1415,7 @@ class DeviceUtilsStartInstrumentationTest(DeviceUtilsTest):
self.call.device.RunShellCommand(
'p=test.package;am instrument -e "$p".foo Foo -e bar \'Val \'"$p" '
'"$p"/.TestInstrumentation',
- check_return=True, large_output=True)):
+ shell=True, check_return=True, large_output=True)):
self.device.StartInstrumentation(
'test.package/.TestInstrumentation',
finish=False, raw=False, extras={'test.package.foo': 'Foo',
@@ -1611,7 +1660,7 @@ class DeviceUtilsPushChangedFilesZippedTest(DeviceUtilsTest):
'/test/temp/file/tmp.zip', '/test/sdcard/foo123.zip'),
self.call.device.RunShellCommand(
'unzip /test/sdcard/foo123.zip&&chmod -R 777 /test/dir',
- as_root=True,
+ shell=True, as_root=True,
env={'PATH': '/data/local/tmp/bin:$PATH'},
check_return=True)):
self.assertTrue(self.device._PushChangedFilesZipped(test_files,
@@ -1632,7 +1681,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest):
def testPathExists_pathExists(self):
with self.assertCall(
self.call.device.RunShellCommand(
- "test -e '/path/file exists'",
+ ['test', '-e', '/path/file exists'],
as_root=False, check_return=True, timeout=10, retries=0),
[]):
self.assertTrue(self.device.PathExists('/path/file exists'))
@@ -1640,7 +1689,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest):
def testPathExists_multiplePathExists(self):
with self.assertCall(
self.call.device.RunShellCommand(
- "test -e '/path 1' -a -e /path2",
+ ['test', '-e', '/path 1', '-a', '-e', '/path2'],
as_root=False, check_return=True, timeout=10, retries=0),
[]):
self.assertTrue(self.device.PathExists(('/path 1', '/path2')))
@@ -1648,7 +1697,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest):
def testPathExists_pathDoesntExist(self):
with self.assertCall(
self.call.device.RunShellCommand(
- "test -e /path/file.not.exists",
+ ['test', '-e', '/path/file.not.exists'],
as_root=False, check_return=True, timeout=10, retries=0),
self.ShellError()):
self.assertFalse(self.device.PathExists('/path/file.not.exists'))
@@ -1656,7 +1705,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest):
def testPathExists_asRoot(self):
with self.assertCall(
self.call.device.RunShellCommand(
- "test -e /root/path/exists",
+ ['test', '-e', '/root/path/exists'],
as_root=True, check_return=True, timeout=10, retries=0),
self.ShellError()):
self.assertFalse(
@@ -1665,7 +1714,7 @@ class DeviceUtilsPathExistsTest(DeviceUtilsTest):
def testFileExists_pathDoesntExist(self):
with self.assertCall(
self.call.device.RunShellCommand(
- "test -e /path/file.not.exists",
+ ['test', '-e', '/path/file.not.exists'],
as_root=False, check_return=True, timeout=10, retries=0),
self.ShellError()):
self.assertFalse(self.device.FileExists('/path/file.not.exists'))
@@ -1829,7 +1878,7 @@ class DeviceUtilsReadFileTest(DeviceUtilsTest):
self.call.device.RunShellCommand(
'SRC=/this/big/file/can.be.read.with.su DEST=/sdcard/tmp/on.device;'
'cp "$SRC" "$DEST" && chmod 666 "$DEST"',
- as_root=True, check_return=True),
+ shell=True, as_root=True, check_return=True),
(self.call.device._ReadFileWithPull('/sdcard/tmp/on.device'),
contents)):
self.assertEqual(
@@ -2154,7 +2203,8 @@ class DeviceUtilsEnsureCacheInitializedTest(DeviceUtilsTest):
self.assertIsNone(self.device._cache['token'])
with self.assertCall(
self.call.device.RunShellCommand(
- AnyStringWith('getprop'), check_return=True, large_output=True),
+ AnyStringWith('getprop'),
+ shell=True, check_return=True, large_output=True),
['/sdcard', 'TOKEN']):
self.device._EnsureCacheInitialized()
self.assertIsNotNone(self.device._cache['token'])
@@ -2163,7 +2213,8 @@ class DeviceUtilsEnsureCacheInitializedTest(DeviceUtilsTest):
self.assertIsNone(self.device._cache['token'])
with self.assertCall(
self.call.device.RunShellCommand(
- AnyStringWith('getprop'), check_return=True, large_output=True),
+ AnyStringWith('getprop'),
+ shell=True, check_return=True, large_output=True),
self.TimeoutError()):
with self.assertRaises(device_errors.CommandTimeoutError):
self.device._EnsureCacheInitialized()
@@ -2232,57 +2283,202 @@ class DeviceUtilsSetPropTest(DeviceUtilsTest):
class DeviceUtilsGetPidsTest(DeviceUtilsTest):
+ def setUp(self):
+ super(DeviceUtilsGetPidsTest, self).setUp()
+ self.sample_output = [
+ 'USER PID PPID VSIZE RSS WCHAN PC NAME',
+ 'user 1001 100 1024 1024 ffffffff 00000000 one.match',
+ 'user 1002 100 1024 1024 ffffffff 00000000 two.match',
+ 'user 1003 100 1024 1024 ffffffff 00000000 three.match',
+ 'user 1234 100 1024 1024 ffffffff 00000000 my$process',
+ 'user 1000 100 1024 1024 ffffffff 00000000 foo',
+ 'user 1236 100 1024 1024 ffffffff 00000000 foo',
+ ]
+
+ def _grepOutput(self, substring):
+ return [line for line in self.sample_output if substring in line]
+
+ def testGetPids_sdkGreaterThanNougatMR1(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=(version_codes.NOUGAT_MR1 + 1)):
+ with self.patch_call(self.call.device.build_id,
+ return_value='ZZZ99Z'):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand(
+ 'ps -e | grep -F example.process'), []):
+ self.device.GetPids('example.process')
def testGetPids_noMatches(self):
- with self.assertCall(
- self.call.device._RunPipedShellCommand('ps | grep -F does.not.match'),
- []):
- self.assertEqual({}, self.device.GetPids('does.not.match'))
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F does.not.match'),
+ self._grepOutput('does.not.match')):
+ self.assertEqual({}, self.device.GetPids('does.not.match'))
def testGetPids_oneMatch(self):
- with self.assertCall(
- self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
- ['user 1001 100 1024 1024 ffffffff 00000000 one.match']):
- self.assertEqual(
- {'one.match': ['1001']},
- self.device.GetPids('one.match'))
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
+ self._grepOutput('one.match')):
+ self.assertEqual(
+ {'one.match': ['1001']},
+ self.device.GetPids('one.match'))
def testGetPids_multipleMatches(self):
- with self.assertCall(
- self.call.device._RunPipedShellCommand('ps | grep -F match'),
- ['user 1001 100 1024 1024 ffffffff 00000000 one.match',
- 'user 1002 100 1024 1024 ffffffff 00000000 two.match',
- 'user 1003 100 1024 1024 ffffffff 00000000 three.match']):
- self.assertEqual(
- {'one.match': ['1001'],
- 'two.match': ['1002'],
- 'three.match': ['1003']},
- self.device.GetPids('match'))
-
- def testGetPids_exactMatch(self):
- with self.assertCall(
- self.call.device._RunPipedShellCommand('ps | grep -F exact.match'),
- ['user 1000 100 1024 1024 ffffffff 00000000 not.exact.match',
- 'user 1234 100 1024 1024 ffffffff 00000000 exact.match']):
- self.assertEqual(
- {'not.exact.match': ['1000'], 'exact.match': ['1234']},
- self.device.GetPids('exact.match'))
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F match'),
+ self._grepOutput('match')):
+ self.assertEqual(
+ {'one.match': ['1001'],
+ 'two.match': ['1002'],
+ 'three.match': ['1003']},
+ self.device.GetPids('match'))
def testGetPids_quotable(self):
- with self.assertCall(
- self.call.device._RunPipedShellCommand("ps | grep -F 'my$process'"),
- ['user 1234 100 1024 1024 ffffffff 00000000 my$process']):
- self.assertEqual(
- {'my$process': ['1234']}, self.device.GetPids('my$process'))
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand("ps | grep -F 'my$process'"),
+ self._grepOutput('my$process')):
+ self.assertEqual(
+ {'my$process': ['1234']}, self.device.GetPids('my$process'))
def testGetPids_multipleInstances(self):
- with self.assertCall(
- self.call.device._RunPipedShellCommand('ps | grep -F foo'),
- ['user 1000 100 1024 1024 ffffffff 00000000 foo',
- 'user 1234 100 1024 1024 ffffffff 00000000 foo']):
- self.assertEqual(
- {'foo': ['1000', '1234']},
- self.device.GetPids('foo'))
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F foo'),
+ self._grepOutput('foo')):
+ self.assertEqual(
+ {'foo': ['1000', '1236']},
+ self.device.GetPids('foo'))
+
+ def testGetPids_allProcesses(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device.RunShellCommand(
+ ['ps'], check_return=True, large_output=True),
+ self.sample_output):
+ self.assertEqual(
+ {'one.match': ['1001'],
+ 'two.match': ['1002'],
+ 'three.match': ['1003'],
+ 'my$process': ['1234'],
+ 'foo': ['1000', '1236']},
+ self.device.GetPids())
+
+ def testGetApplicationPids_notFound(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F match'),
+ self._grepOutput('match')):
+ # No PIDs found, process name should be exact match.
+ self.assertEqual([], self.device.GetApplicationPids('match'))
+
+ def testGetApplicationPids_foundOne(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
+ self._grepOutput('one.match')):
+ self.assertEqual(['1001'], self.device.GetApplicationPids('one.match'))
+
+ def testGetApplicationPids_foundMany(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F foo'),
+ self._grepOutput('foo')):
+ self.assertEqual(
+ ['1000', '1236'],
+ self.device.GetApplicationPids('foo'))
+
+ def testGetApplicationPids_atMostOneNotFound(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F match'),
+ self._grepOutput('match')):
+ # No PIDs found, process name should be exact match.
+ self.assertEqual(
+ None,
+ self.device.GetApplicationPids('match', at_most_one=True))
+
+ def testGetApplicationPids_atMostOneFound(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F one.match'),
+ self._grepOutput('one.match')):
+ self.assertEqual(
+ '1001',
+ self.device.GetApplicationPids('one.match', at_most_one=True))
+
+ def testGetApplicationPids_atMostOneFoundTooMany(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.LOLLIPOP):
+ with self.assertRaises(device_errors.CommandFailedError):
+ with self.assertCall(
+ self.call.device._RunPipedShellCommand('ps | grep -F foo'),
+ self._grepOutput('foo')):
+ self.device.GetApplicationPids('foo', at_most_one=True)
+
+
+class DeviceUtilsGetSetEnforce(DeviceUtilsTest):
+
+ def testGetEnforce_Enforcing(self):
+ with self.assertCall(self.call.adb.Shell('getenforce'), 'Enforcing'):
+ self.assertEqual(True, self.device.GetEnforce())
+
+ def testGetEnforce_Permissive(self):
+ with self.assertCall(self.call.adb.Shell('getenforce'), 'Permissive'):
+ self.assertEqual(False, self.device.GetEnforce())
+
+ def testGetEnforce_Disabled(self):
+ with self.assertCall(self.call.adb.Shell('getenforce'), 'Disabled'):
+ self.assertEqual(None, self.device.GetEnforce())
+
+ def testSetEnforce_Enforcing(self):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), False),
+ (self.call.adb.Shell('setenforce 1'), '')):
+ self.device.SetEnforce(enabled=True)
+
+ def testSetEnforce_Permissive(self):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), False),
+ (self.call.adb.Shell('setenforce 0'), '')):
+ self.device.SetEnforce(enabled=False)
+
+ def testSetEnforce_EnforcingWithInt(self):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), False),
+ (self.call.adb.Shell('setenforce 1'), '')):
+ self.device.SetEnforce(enabled=1)
+
+ def testSetEnforce_PermissiveWithInt(self):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), False),
+ (self.call.adb.Shell('setenforce 0'), '')):
+ self.device.SetEnforce(enabled=0)
+
+ def testSetEnforce_EnforcingWithStr(self):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), False),
+ (self.call.adb.Shell('setenforce 1'), '')):
+ self.device.SetEnforce(enabled='1')
+
+ def testSetEnforce_PermissiveWithStr(self):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), False),
+ (self.call.adb.Shell('setenforce 0'), '')):
+ self.device.SetEnforce(enabled='0') # Not recommended but it works!
class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest):
@@ -2539,7 +2735,7 @@ class DeviceUtilsRestartAdbdTest(DeviceUtilsTest):
self.adb, suffix='.sh'), MockTempFile(mock_temp_file)),
self.call.device.WriteFile(mock.ANY, mock.ANY),
(self.call.device.RunShellCommand(
- ['source', mock_temp_file], as_root=True)),
+ ['source', mock_temp_file], check_return=True, as_root=True)),
self.call.adb.WaitForDevice()):
self.device.RestartAdbd()
@@ -2560,7 +2756,7 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest):
return_value=version_codes.MARSHMALLOW):
with self.assertCalls(
(self.call.device.RunShellCommand(
- permissions_cmd, check_return=True), [])):
+ permissions_cmd, shell=True, check_return=True), [])):
self.device.GrantPermissions('package', ['p1'])
def testGrantPermissions_multiple(self):
@@ -2569,7 +2765,7 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest):
return_value=version_codes.MARSHMALLOW):
with self.assertCalls(
(self.call.device.RunShellCommand(
- permissions_cmd, check_return=True), [])):
+ permissions_cmd, shell=True, check_return=True), [])):
self.device.GrantPermissions('package', ['p1', 'p2'])
def testGrantPermissions_WriteExtrnalStorage(self):
@@ -2580,7 +2776,7 @@ class DeviceUtilsGrantPermissionsTest(DeviceUtilsTest):
return_value=version_codes.MARSHMALLOW):
with self.assertCalls(
(self.call.device.RunShellCommand(
- permissions_cmd, check_return=True), [])):
+ permissions_cmd, shell=True, check_return=True), [])):
self.device.GrantPermissions(
'package', ['android.permission.WRITE_EXTERNAL_STORAGE'])
@@ -2652,7 +2848,7 @@ class DeviecUtilsSetScreen(DeviceUtilsTest):
def testSetScreen_on(self):
with self.assertCalls(
(self.call.device.IsScreenOn(), False),
- (self.call.device.RunShellCommand('input keyevent 26'), []),
+ (self.call.device.SendKeyEvent(keyevent.KEYCODE_POWER), None),
(self.call.device.IsScreenOn(), True)):
self.device.SetScreen(True)
@@ -2660,7 +2856,7 @@ class DeviecUtilsSetScreen(DeviceUtilsTest):
def testSetScreen_off(self):
with self.assertCalls(
(self.call.device.IsScreenOn(), True),
- (self.call.device.RunShellCommand('input keyevent 26'), []),
+ (self.call.device.SendKeyEvent(keyevent.KEYCODE_POWER), None),
(self.call.device.IsScreenOn(), False)):
self.device.SetScreen(False)
@@ -2668,7 +2864,7 @@ class DeviecUtilsSetScreen(DeviceUtilsTest):
def testSetScreen_slow(self):
with self.assertCalls(
(self.call.device.IsScreenOn(), True),
- (self.call.device.RunShellCommand('input keyevent 26'), []),
+ (self.call.device.SendKeyEvent(keyevent.KEYCODE_POWER), None),
(self.call.device.IsScreenOn(), True),
(self.call.device.IsScreenOn(), True),
(self.call.device.IsScreenOn(), False)):
diff --git a/catapult/devil/devil/android/flag_changer.py b/catapult/devil/devil/android/flag_changer.py
index 2c8cc3c2..b2ee8b16 100644
--- a/catapult/devil/devil/android/flag_changer.py
+++ b/catapult/devil/devil/android/flag_changer.py
@@ -2,10 +2,14 @@
# 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__)
@@ -16,6 +20,42 @@ _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.
+ """
+ # On Android N and above, we need to temporarily set SELinux to permissive
+ # so that Chrome is allowed to read the command line file.
+ # TODO(crbug.com/699082): Remove when a solution to avoid this is implemented.
+ needs_permissive = (
+ device.build_version_sdk >= version_codes.NOUGAT and
+ device.GetEnforce())
+ if needs_permissive:
+ device.SetEnforce(enabled=False)
+ try:
+ changer = FlagChanger(device, cmdline_name)
+ try:
+ changer.ReplaceFlags(flags)
+ yield
+ finally:
+ changer.Restore()
+ finally:
+ if needs_permissive:
+ device.SetEnforce(enabled=True)
+
+
class FlagChanger(object):
"""Changes the flags Chrome runs with.
@@ -33,21 +73,13 @@ class FlagChanger(object):
"""
self._device = device
- unused_dir, basename = posixpath.split(cmdline_file)
- self._cmdline_path = posixpath.join(_CMDLINE_DIR, basename)
+ 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)
+ self._cmdline_path = posixpath.join(_CMDLINE_DIR, cmdline_file)
- # TODO(catapult:#3112): Make this fail instead of warn after all clients
- # have been switched.
- if unused_dir:
- logging.warning(
- 'cmdline_file argument of %s() should be a file name only (not a'
- ' full path).', type(self).__name__)
- if cmdline_file != self._cmdline_path:
- logging.warning(
- 'Client supplied %r, but %r will be used instead.',
- cmdline_file, self._cmdline_path)
-
- cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, basename)
+ cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file)
if self._device.PathExists(cmdline_path_legacy):
logging.warning(
'Removing legacy command line file %r.', cmdline_path_legacy)
@@ -168,7 +200,7 @@ class FlagChanger(object):
if command_line is not None:
self._device.WriteFile(self._cmdline_path, command_line)
else:
- self._device.RunShellCommand('rm ' + self._cmdline_path)
+ self._device.RemovePath(self._cmdline_path, force=True)
current_flags = self.GetCurrentFlags()
logger.info('Flags now set on the device: %s', current_flags)
@@ -262,14 +294,7 @@ def _QuoteFlag(flag):
if value is None:
return key
- else:
- # TODO(catapult:#3112): Remove this check when all clients comply.
- if value[0] in _QUOTES and value[0] == value[-1]:
- logging.warning(
- 'Flag %s appears to be quoted, so will be passed as-is.', flag)
- logging.warning(
- 'Note: this behavior will be changed in the future. '
- 'Clients should pass values unquoted to prevent double-quoting.')
- elif _RE_NEEDS_QUOTING.search(value):
- value = '"%s"' % value.replace('"', r'\"')
- return '='.join([key, value])
+
+ if _RE_NEEDS_QUOTING.search(value):
+ value = '"%s"' % value.replace('"', r'\"')
+ return '='.join([key, value])
diff --git a/catapult/devil/devil/android/flag_changer_devicetest.py b/catapult/devil/devil/android/flag_changer_devicetest.py
index f5d19d60..b75504b5 100644
--- a/catapult/devil/devil/android/flag_changer_devicetest.py
+++ b/catapult/devil/devil/android/flag_changer_devicetest.py
@@ -39,10 +39,11 @@ class FlagChangerTest(device_test_case.DeviceTestCase):
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(self):
+ def testFlagChanger_restoreFlags(self):
if not self.device.HasRoot():
self.skipTest('Test needs a rooted device')
@@ -72,6 +73,16 @@ class FlagChangerTest(device_test_case.DeviceTestCase):
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()
diff --git a/catapult/devil/devil/android/flag_changer_test.py b/catapult/devil/devil/android/flag_changer_test.py
index f692bd62..5342cf44 100755
--- a/catapult/devil/devil/android/flag_changer_test.py
+++ b/catapult/devil/devil/android/flag_changer_test.py
@@ -52,6 +52,10 @@ class FlagChangerTest(unittest.TestCase):
self.cmdline_path)
self.assertFalse(self.device.PathExists(self.cmdline_path_legacy))
+ 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):
@@ -82,18 +86,17 @@ class ParseSerializeFlagsTest(unittest.TestCase):
"--key=this is 'fine' too", '''--key="this is 'fine' too"''')
def testQuoteFlag_withQuotedValue4(self):
- with self.assertRaises(AssertionError):
- # TODO(catapult:#3112) This test is broken in the current implementation;
- # flags that appear to be quoted are left as-is and, thus, do not
- # survive the round-trip.
- self._testQuoteFlag(
- "--key='I really want to keep these quotes'",
- '''--key="'I really want to keep these quotes'"''')
+ 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
diff --git a/catapult/devil/devil/android/forwarder.py b/catapult/devil/devil/android/forwarder.py
index b5a2bf14..244f555a 100644
--- a/catapult/devil/devil/android/forwarder.py
+++ b/catapult/devil/devil/android/forwarder.py
@@ -13,6 +13,7 @@ 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
@@ -130,7 +131,7 @@ class Forwarder(object):
device_serial = str(device)
map_arg_lists = [
- ['--adb=' + devil_env.config.FetchPath('adb'),
+ ['--adb=' + adb_wrapper.AdbWrapper.GetAdbPath(),
'--serial-id=' + device_serial,
'--map', str(device_port), str(host_port)]
for device_port, host_port in port_pairs]
@@ -203,7 +204,7 @@ class Forwarder(object):
instance = Forwarder._GetInstanceLocked(None)
unmap_all_cmd = [
instance._host_forwarder_path,
- '--adb=%s' % devil_env.config.FetchPath('adb'),
+ '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
'--serial-id=%s' % device.serial,
'--unmap-all'
]
@@ -307,7 +308,7 @@ class Forwarder(object):
unmap_cmd = [
instance._host_forwarder_path,
- '--adb=%s' % devil_env.config.FetchPath('adb'),
+ '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
'--serial-id=%s' % serial,
'--unmap', str(device_port)
]
@@ -387,7 +388,10 @@ class Forwarder(object):
forwarder_device_path_on_host,
forwarder_device_path_on_device)])
- cmd = '%s %s' % (tool.GetUtilWrapper(), Forwarder._DEVICE_FORWARDER_PATH)
+ 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)
@@ -451,8 +455,10 @@ class Forwarder(object):
if not device.FileExists(Forwarder._DEVICE_FORWARDER_PATH):
return
- cmd = '%s %s --kill-server' % (tool.GetUtilWrapper(),
- Forwarder._DEVICE_FORWARDER_PATH)
+ 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)
diff --git a/catapult/devil/devil/android/install_commands.py b/catapult/devil/devil/android/install_commands.py
index 5a06bf3f..c8da8696 100644
--- a/catapult/devil/devil/android/install_commands.py
+++ b/catapult/devil/devil/android/install_commands.py
@@ -42,7 +42,8 @@ def InstallCommands(device):
'%s not found. Please build chromium_commands.'
% chromium_commands_jar_path)
- device.RunShellCommand(['mkdir', BIN_DIR, _FRAMEWORK_DIR])
+ 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)
@@ -54,4 +55,3 @@ def InstallCommands(device):
device.adb.Push(
chromium_commands_jar_path,
'%s/chromium_commands.jar' % _FRAMEWORK_DIR)
-
diff --git a/catapult/devil/devil/android/logcat_monitor.py b/catapult/devil/devil/android/logcat_monitor.py
index c99fda9f..0aece87d 100644
--- a/catapult/devil/devil/android/logcat_monitor.py
+++ b/catapult/devil/devil/android/logcat_monitor.py
@@ -23,7 +23,8 @@ logger = logging.getLogger(__name__)
class LogcatMonitor(object):
- _RECORD_THREAD_JOIN_WAIT = 2.0
+ _RECORD_ITER_TIMEOUT = 2.0
+ _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) +'
@@ -164,10 +165,16 @@ class LogcatMonitor(object):
# 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'):
+ logcat_format='threadtime',
+ iter_timeout=self._RECORD_ITER_TIMEOUT):
+ 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._stop_recording_event.isSet():
- return
if self._record_file and not self._record_file.closed:
self._record_file.write(data + '\n')
diff --git a/catapult/devil/devil/android/md5sum.py b/catapult/devil/devil/android/md5sum.py
index 52706461..6dece9e8 100644
--- a/catapult/devil/devil/android/md5sum.py
+++ b/catapult/devil/devil/android/md5sum.py
@@ -89,7 +89,7 @@ def CalculateDeviceMd5Sums(paths, device):
# Note: ":" is equivalent to "true".
md5sum_script += ';:'
try:
- out = device.RunShellCommand(md5sum_script, check_return=True)
+ out = device.RunShellCommand(md5sum_script, shell=True, check_return=True)
except device_errors.AdbShellCommandFailedError as e:
# Push the binary only if it is found to not exist
# (faster than checking up-front).
@@ -103,10 +103,10 @@ def CalculateDeviceMd5Sums(paths, device):
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, check_return=True)
+ 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, check_return=True)
+ out = device.RunShellCommand(md5sum_script, shell=True, check_return=True)
else:
raise
diff --git a/catapult/devil/devil/android/perf/cache_control.py b/catapult/devil/devil/android/perf/cache_control.py
index 7bd0a4e7..27782b50 100644
--- a/catapult/devil/devil/android/perf/cache_control.py
+++ b/catapult/devil/devil/android/perf/cache_control.py
@@ -11,6 +11,5 @@ class CacheControl(object):
def DropRamCaches(self):
"""Drops the filesystem ram caches for performance testing."""
- self._device.RunShellCommand('sync', as_root=True)
+ self._device.RunShellCommand(['sync'], check_return=True, as_root=True)
self._device.WriteFile(CacheControl._DROP_CACHES, '3', as_root=True)
-
diff --git a/catapult/devil/devil/android/perf/perf_control.py b/catapult/devil/devil/android/perf/perf_control.py
index d52d64e6..06a5db61 100644
--- a/catapult/devil/devil/android/perf/perf_control.py
+++ b/catapult/devil/devil/android/perf/perf_control.py
@@ -111,7 +111,7 @@ class PerfControl(object):
'done'
])
output = self._device.RunShellCommand(
- script, cwd=self._CPU_PATH, check_return=True, as_root=True)
+ 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]))
diff --git a/catapult/devil/devil/android/perf/surface_stats_collector.py b/catapult/devil/devil/android/perf/surface_stats_collector.py
index 49372ad2..25079f31 100644
--- a/catapult/devil/devil/android/perf/surface_stats_collector.py
+++ b/catapult/devil/devil/android/perf/surface_stats_collector.py
@@ -105,15 +105,17 @@ class SurfaceStatsCollector(object):
# 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')
+ ['dumpsys', 'SurfaceFlinger', '--latency-clear', 'SurfaceView'],
+ check_return=True)
return not len(results)
def GetSurfaceFlingerPid(self):
- results = self._device.RunShellCommand('ps | grep surfaceflinger')
- if not results:
+ pids_dict = self._device.GetPids('surfaceflinger')
+ if not pids_dict:
raise Exception('Unable to get surface flinger process id')
- pid = results[0].split()[1]
- return pid
+ # TODO(cataput:#3378): Do more strict checks in GetPids when possible.
+ # For now it just returns the first pid found of some matching process.
+ return pids_dict.popitem()[1][0]
def _GetSurfaceFlingerFrameData(self):
"""Returns collected SurfaceFlinger frame timing data.
@@ -156,7 +158,8 @@ class SurfaceStatsCollector(object):
# the activity's main window are not updated when the main web content is
# composited into a SurfaceView.
results = self._device.RunShellCommand(
- 'dumpsys SurfaceFlinger --latency SurfaceView')
+ ['dumpsys', 'SurfaceFlinger', '--latency', 'SurfaceView'],
+ check_return=True)
if not len(results):
return (None, None)
diff --git a/catapult/devil/devil/android/perf/thermal_throttle.py b/catapult/devil/devil/android/perf/thermal_throttle.py
index 5a0454ec..546a92e0 100644
--- a/catapult/devil/devil/android/perf/thermal_throttle.py
+++ b/catapult/devil/devil/android/perf/thermal_throttle.py
@@ -124,7 +124,8 @@ class ThermalThrottle(object):
serial_number, temperature, degree_symbol)
# Print temperature of battery, to give a system temperature
- dumpsys_log = self._device.RunShellCommand('dumpsys battery')
+ 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
diff --git a/catapult/devil/devil/android/sdk/adb_wrapper.py b/catapult/devil/devil/android/sdk/adb_wrapper.py
index 8654d269..7f6b8d95 100644
--- a/catapult/devil/devil/android/sdk/adb_wrapper.py
+++ b/catapult/devil/devil/android/sdk/adb_wrapper.py
@@ -38,8 +38,8 @@ DEFAULT_RETRIES = 2
_ADB_VERSION_RE = re.compile(r'Android Debug Bridge version (\d+\.\d+\.\d+)')
_EMULATOR_RE = re.compile(r'^emulator-[0-9]+$')
_READY_STATE = 'device'
-_VERITY_DISABLE_RE = re.compile('Verity (already)? disabled')
-_VERITY_ENABLE_RE = re.compile('Verity (already)? enabled')
+_VERITY_DISABLE_RE = re.compile(r'Verity (already )?disabled')
+_VERITY_ENABLE_RE = re.compile(r'Verity (already )?enabled')
def VerifyLocalFileExists(path):
@@ -281,18 +281,21 @@ class AdbWrapper(object):
device_serial=self._device_serial,
check_error=check_error)
- def _IterRunDeviceAdbCmd(self, args, timeout):
+ def _IterRunDeviceAdbCmd(self, args, iter_timeout, timeout):
"""Runs an adb command and returns an iterator over its output lines.
Args:
args: A list of arguments to adb.
- timeout: Timeout in seconds.
+ iter_timeout: Timeout for each iteration in seconds.
+ timeout: Timeout for the entire command in seconds.
Yields:
The output of the command line by line.
"""
return cmd_helper.IterCmdOutputLines(
- self._BuildAdbCmd(args, self._device_serial), timeout=timeout)
+ self._BuildAdbCmd(args, self._device_serial),
+ iter_timeout=iter_timeout,
+ timeout=timeout)
def __eq__(self, other):
"""Consider instances equal if they refer to the same device.
@@ -453,7 +456,9 @@ class AdbWrapper(object):
VerifyLocalFileExists(local)
except IOError:
raise device_errors.AdbCommandFailedError(
- cmd, 'File not found on host: %s' % local, device_serial=str(self))
+ cmd,
+ 'File pulled from the device did not arrive on the host: %s' % local,
+ device_serial=str(self))
def Shell(self, command, expect_status=0, timeout=DEFAULT_TIMEOUT,
retries=DEFAULT_RETRIES):
@@ -548,8 +553,8 @@ class AdbWrapper(object):
device_serial=self._device_serial)
def Logcat(self, clear=False, dump=False, filter_specs=None,
- logcat_format=None, ring_buffer=None, timeout=None,
- retries=DEFAULT_RETRIES):
+ logcat_format=None, ring_buffer=None, iter_timeout=None,
+ timeout=None, retries=DEFAULT_RETRIES):
"""Get an iterable over the logcat output.
Args:
@@ -562,6 +567,9 @@ class AdbWrapper(object):
ring_buffer: If set, a list of alternate ring buffers to request.
Options include "main", "system", "radio", "events", "crash" or "all".
The default is equivalent to ["main", "system", "crash"].
+ iter_timeout: If set and neither clear nor dump is set, the number of
+ seconds to wait between iterations. If no line is found before the
+ given number of seconds elapses, the iterable will yield None.
timeout: (optional) If set, timeout per try in seconds. If clear or dump
is set, defaults to DEFAULT_TIMEOUT.
retries: (optional) If clear or dump is set, the number of retries to
@@ -587,7 +595,7 @@ class AdbWrapper(object):
cmd.extend(filter_specs)
if use_iter:
- return self._IterRunDeviceAdbCmd(cmd, timeout)
+ return self._IterRunDeviceAdbCmd(cmd, iter_timeout, timeout)
else:
timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines()
@@ -745,7 +753,7 @@ class AdbWrapper(object):
cmd.append('-k')
cmd.append(package)
output = self._RunDeviceAdbCmd(cmd, timeout, retries)
- if 'Failure' in output:
+ if 'Failure' in output or 'Exception' in output:
raise device_errors.AdbCommandFailedError(
cmd, output, device_serial=self._device_serial)
@@ -886,14 +894,14 @@ class AdbWrapper(object):
def DisableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
"""Disable Marshmallow's Verity security feature"""
output = self._RunDeviceAdbCmd(['disable-verity'], timeout, retries)
- if output and _VERITY_DISABLE_RE.search(output):
+ if output and not _VERITY_DISABLE_RE.search(output):
raise device_errors.AdbCommandFailedError(
['disable-verity'], output, device_serial=self._device_serial)
def EnableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
"""Enable Marshmallow's Verity security feature"""
output = self._RunDeviceAdbCmd(['enable-verity'], timeout, retries)
- if output and _VERITY_ENABLE_RE.search(output):
+ if output and not _VERITY_ENABLE_RE.search(output):
raise device_errors.AdbCommandFailedError(
['enable-verity'], output, device_serial=self._device_serial)
diff --git a/catapult/devil/devil/android/sdk/adb_wrapper_test.py b/catapult/devil/devil/android/sdk/adb_wrapper_test.py
new file mode 100755
index 00000000..ef086612
--- /dev/null
+++ b/catapult/devil/devil/android/sdk/adb_wrapper_test.py
@@ -0,0 +1,59 @@
+#!/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.adb = adb_wrapper.AdbWrapper('ABC12345678')
+
+ 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)
+
diff --git a/catapult/devil/devil/android/sdk/keyevent.py b/catapult/devil/devil/android/sdk/keyevent.py
index 40f9416c..657dc963 100644
--- a/catapult/devil/devil/android/sdk/keyevent.py
+++ b/catapult/devil/devil/android/sdk/keyevent.py
@@ -22,6 +22,8 @@ KEYCODE_9 = 16
KEYCODE_DPAD_RIGHT = 22
+KEYCODE_POWER = 26
+
KEYCODE_A = 29
KEYCODE_B = 30
KEYCODE_C = 31
diff --git a/catapult/devil/devil/android/sdk/shared_prefs.py b/catapult/devil/devil/android/sdk/shared_prefs.py
index 58370bc0..2fa2e6a1 100644
--- a/catapult/devil/devil/android/sdk/shared_prefs.py
+++ b/catapult/devil/devil/android/sdk/shared_prefs.py
@@ -11,6 +11,8 @@ See e.g.:
import logging
import posixpath
+from devil.android import device_errors
+from devil.android.sdk import version_codes
from xml.etree import ElementTree
logger = logging.getLogger(__name__)
@@ -270,6 +272,19 @@ class SharedPrefs(object):
['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._GetSecurityContext(self.package)
+ if security_context == None:
+ raise device_errors.CommandFailedError(
+ 'Failed to get security context for %s' % self.package)
+ self._device.RunShellCommand(
+ ['chcon', '-R', security_context,
+ '/data/data/%s/shared_prefs' % self.package],
+ as_root=True, check_return=True)
self._device.KillAll(self.package, exact=True, as_root=True, quiet=True)
self._changed = False
@@ -391,3 +406,15 @@ class SharedPrefs(object):
pref.set(value)
self._changed = True
logger.info('Setting property: %s', pref)
+
+ def _GetSecurityContext(self, package):
+ for line in self._device.RunShellCommand(['ls', '-Z', '/data/data/'],
+ as_root=True, check_return=True):
+ split_line = line.split()
+ # ls -Z output differs between Android versions, but the package is
+ # always last and the context always starts with "u:object"
+ if split_line[-1] == package:
+ for column in split_line:
+ if column.startswith('u:object'):
+ return column
+ return None
diff --git a/catapult/devil/devil/android/sdk/shared_prefs_test.py b/catapult/devil/devil/android/sdk/shared_prefs_test.py
index ff3b9a13..4c31c569 100755
--- a/catapult/devil/devil/android/sdk/shared_prefs_test.py
+++ b/catapult/devil/devil/android/sdk/shared_prefs_test.py
@@ -13,6 +13,7 @@ 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
@@ -97,6 +98,8 @@ class SharedPrefsTest(unittest.TestCase):
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
@@ -131,6 +134,8 @@ class SharedPrefsTest(unittest.TestCase):
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)
diff --git a/catapult/devil/devil/android/sdk/version_codes.py b/catapult/devil/devil/android/sdk/version_codes.py
index 095b59cc..3f03cbac 100644
--- a/catapult/devil/devil/android/sdk/version_codes.py
+++ b/catapult/devil/devil/android/sdk/version_codes.py
@@ -16,4 +16,5 @@ LOLLIPOP = 21
LOLLIPOP_MR1 = 22
MARSHMALLOW = 23
NOUGAT = 24
+NOUGAT_MR1 = 25
diff --git a/catapult/devil/devil/android/settings.py b/catapult/devil/devil/android/settings.py
index 427592f2..886b2661 100644
--- a/catapult/devil/devil/android/settings.py
+++ b/catapult/devil/devil/android/settings.py
@@ -141,7 +141,8 @@ class ContentSettings(dict):
# Example row:
# 'Row: 0 _id=13, name=logging_id2, value=-1fccbaa546705b05'
for row in self._device.RunShellCommand(
- 'content query --uri content://%s' % self._table, as_root=True):
+ ['content', 'query', '--uri', 'content://%s' % self._table],
+ check_return=True, as_root=True):
fields = row.split(', ')
key = None
value = None
@@ -159,33 +160,29 @@ class ContentSettings(dict):
def __getitem__(self, key):
return self._device.RunShellCommand(
- 'content query --uri content://%s --where "name=\'%s\'" '
- '--projection value' % (self._table, key), as_root=True).strip()
+ ['content', 'query', '--uri', 'content://%s' % self._table,
+ '--where', "name='%s'" % key],
+ check_return=True, as_root=True).strip()
def __setitem__(self, key, value):
if key in self:
self._device.RunShellCommand(
- 'content update --uri content://%s '
- '--bind value:%s:%s --where "name=\'%s\'"' % (
- self._table,
- self._GetTypeBinding(value), value, key),
- as_root=True)
+ ['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 '
- '--bind name:%s:%s --bind value:%s:%s' % (
- self._table,
- self._GetTypeBinding(key), key,
- self._GetTypeBinding(value), value),
- as_root=True)
+ ['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 '
- '--bind name:%s:%s' % (
- self._table,
- self._GetTypeBinding(key), key),
- as_root=True)
+ ['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):
@@ -270,8 +267,7 @@ commit transaction;""" % {
'columns': ', '.join(columns),
'values': ', '.join(["'%s'" % value for value in values])
}
- output_msg = device.RunShellCommand('sqlite3 %s "%s"' % (db, cmd),
- as_root=True)
+ output_msg = device.RunShellCommand(
+ ['sqlite3', db, cmd], check_return=True, as_root=True)
if output_msg:
logger.info(' '.join(output_msg))
-
diff --git a/catapult/devil/devil/android/tools/device_monitor.py b/catapult/devil/devil/android/tools/device_monitor.py
index 6139c770..49214a92 100755
--- a/catapult/devil/devil/android/tools/device_monitor.py
+++ b/catapult/devil/devil/android/tools/device_monitor.py
@@ -16,6 +16,7 @@ import logging
import logging.handlers
import os
import re
+import socket
import sys
import time
@@ -42,8 +43,15 @@ CPU_TEMP_SENSORS = [
]
DEVICE_FILE_VERSION = 1
-DEVICE_FILE = os.path.join(os.path.expanduser('~'),
- 'android_device_status.json')
+# TODO(bpastene): Remove the old file once sysmon has been updated to read the
+# new status file.
+DEVICE_FILES = [
+ os.path.join(os.path.expanduser('~'), 'android_device_status.json'),
+ 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'
@@ -116,7 +124,8 @@ def get_device_status(device):
# Process
try:
- lines = device.RunShellCommand('ps', check_return=True)
+ # TODO(catapult:#3215): Migrate to device.GetPids()
+ lines = device.RunShellCommand(['ps'], check_return=True)
status['processes'] = len(lines) - 1 # Ignore the header row.
except device_errors.AdbShellCommandFailedError:
logging.exception('Unable to count process list.')
@@ -128,7 +137,7 @@ def get_device_status(device):
try:
files = device.RunShellCommand(
'grep -lE "%s" /sys/class/thermal/thermal_zone*/type' % '|'.join(
- CPU_TEMP_SENSORS), check_return=True)
+ CPU_TEMP_SENSORS), shell=True, check_return=True)
except device_errors.AdbShellCommandFailedError:
logging.exception('Unable to list thermal sensors.')
for f in files:
@@ -211,8 +220,9 @@ def main(argv):
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)
+ for device_file in DEVICE_FILES:
+ 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)
diff --git a/catapult/devil/devil/android/tools/device_monitor_test.py b/catapult/devil/devil/android/tools/device_monitor_test.py
index 9c597938..e39e324b 100755
--- a/catapult/devil/devil/android/tools/device_monitor_test.py
+++ b/catapult/devil/devil/android/tools/device_monitor_test.py
@@ -43,8 +43,15 @@ class DeviceMonitorTest(unittest.TestCase):
'ps': ['headers', 'p1', 'p2', 'p3', 'p4', 'p5'],
'grep': ['/sys/class/thermal/thermal_zone0/type'],
}
- self.device.RunShellCommand = mock.MagicMock(
- side_effect=lambda cmd, **kwargs: self.cmd_outputs[cmd.split()[0]])
+
+ 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(
@@ -104,14 +111,7 @@ class DeviceMonitorTest(unittest.TestCase):
def test_getStatsNoPs(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
- def _throw_on_ps(cmd):
- if cmd == 'ps':
- raise device_errors.AdbShellCommandFailedError(cmd, None, None)
- else:
- return []
- self.device.RunShellCommand = mock.MagicMock(
- side_effect=lambda cmd, **kwargs:
- _throw_on_ps(cmd) + self.cmd_outputs[cmd.split()[0]])
+ del self.cmd_outputs['ps'] # Throw exception on run shell ps command.
# Should be same status dict but without process stats.
expected_status_no_ps = self.expected_status.copy()
@@ -125,14 +125,7 @@ class DeviceMonitorTest(unittest.TestCase):
def test_getStatsNoSensors(self, get_devices, get_battery):
get_devices.return_value = [self.device]
get_battery.return_value = self.battery
- def _throw_on_grep(cmd):
- if cmd.startswith('grep'):
- raise device_errors.AdbShellCommandFailedError(cmd, None, None)
- else:
- return []
- self.device.RunShellCommand = mock.MagicMock(
- side_effect=lambda cmd, **kwargs:
- _throw_on_grep(cmd) + self.cmd_outputs[cmd.split()[0]])
+ 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()
@@ -173,4 +166,3 @@ class DeviceMonitorTest(unittest.TestCase):
if __name__ == '__main__':
sys.exit(unittest.main())
-
diff --git a/catapult/devil/devil/android/tools/provision_devices.py b/catapult/devil/devil/android/tools/provision_devices.py
index df75539f..7374290c 100755
--- a/catapult/devil/devil/android/tools/provision_devices.py
+++ b/catapult/devil/devil/android/tools/provision_devices.py
@@ -49,7 +49,8 @@ from devil.utils import timeout_retry
logger = logging.getLogger(__name__)
-_SYSTEM_WEBVIEW_PATHS = ['/system/app/webview', '/system/app/WebViewGoogle']
+_SYSTEM_APP_DIRECTORIES = ['/system/app/', '/system/priv-app/']
+_SYSTEM_WEBVIEW_NAMES = ['webview', 'WebViewGoogle']
_CHROME_PACKAGE_REGEX = re.compile('.*chrom.*')
_TOMBSTONE_REGEX = re.compile('tombstone.*')
@@ -84,10 +85,12 @@ def ProvisionDevices(
output_device_blacklist=None,
reboot_timeout=None,
remove_system_webview=False,
+ system_app_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 []
try:
devices = script_common.GetDevices(devices, blacklist)
except device_errors.NoDevicesError:
@@ -122,7 +125,11 @@ def ProvisionDevices(
lambda d: WaitForCharge(d, min_battery_level)))
if remove_system_webview:
- steps.append(ProvisionStep(RemoveSystemWebView))
+ system_app_remove_list.extend(_SYSTEM_WEBVIEW_NAMES)
+
+ if system_app_remove_list:
+ steps.append(ProvisionStep(
+ lambda d: RemoveSystemApps(d, system_app_remove_list)))
steps.append(ProvisionStep(SetDate))
steps.append(ProvisionStep(CheckExternalStorage))
@@ -204,8 +211,9 @@ def WipeChromeData(device):
_UninstallIfMatch(device, _CHROME_PACKAGE_REGEX,
chrome.PACKAGE_INFO['chrome_stable'].package)
device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(),
- check_return=True)
- device.RunShellCommand('rm -rf /data/local/tmp/*', check_return=True)
+ 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,
@@ -218,17 +226,18 @@ def WipeChromeData(device):
_WipeFileOrDir(device, '/data/local/.config/')
_WipeFileOrDir(device, '/data/local/tmp/')
device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(),
- check_return=True)
+ shell=True, check_return=True)
except device_errors.CommandFailedError:
logger.exception('Possible failure while wiping the device. '
'Attempting to continue.')
def _UninstallIfMatch(device, pattern, app_to_keep):
- installed_packages = device.RunShellCommand(['pm', 'list', 'packages'])
+ 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'])]
+ 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 not package == app_to_keep:
@@ -343,28 +352,43 @@ def DisableSystemChrome(device):
check_return=True)
-def RemoveSystemWebView(device):
- if any(device.PathExists(p) for p in _SYSTEM_WEBVIEW_PATHS):
- logger.info('System WebView exists and needs to be removed')
- if device.HasRoot():
- # Disabled Marshmallow's Verity security feature
- if device.build_version_sdk >= version_codes.MARSHMALLOW:
- device.adb.DisableVerity()
- device.Reboot()
- device.WaitUntilFullyBooted()
- device.EnableRoot()
-
- # This is required, e.g., to replace the system webview on a device.
- device.adb.Remount()
- device.RunShellCommand(['stop'], check_return=True)
- device.RunShellCommand(['rm', '-rf'] + _SYSTEM_WEBVIEW_PATHS,
- check_return=True)
- device.RunShellCommand(['start'], check_return=True)
- else:
- logger.warning('Cannot remove system webview from a non-rooted device')
- else:
- logger.info('System WebView already removed')
+def _RemoveSystemApp(device, system_app):
+ found_paths = []
+ for directory in _SYSTEM_APP_DIRECTORIES:
+ path = os.path.join(directory, system_app)
+ if device.PathExists(path):
+ found_paths.append(path)
+ if not found_paths:
+ logger.warning('Could not find install location for system app %s',
+ system_app)
+ device.RemovePath(found_paths, force=True, recursive=True)
+def RemoveSystemApps(device, system_app_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']
+ """
+ device.EnableRoot()
+ if device.HasRoot():
+ # 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)
+ for system_app in system_app_remove_list:
+ _RemoveSystemApp(device, system_app)
+ 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):
@@ -435,7 +459,8 @@ def SetDate(device):
get_date_command.append('+"%Y%m%d.%H%M%S"')
device_time = device.RunShellCommand(
- get_date_command, as_root=True, single_line=True).replace('"', '')
+ 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
@@ -456,12 +481,13 @@ def SetDate(device):
_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()
device.BroadcastIntent(
intent.Intent(action='android.intent.action.TIME_SET'))
def LogDeviceProperties(device):
- props = device.RunShellCommand('getprop', check_return=True)
+ props = device.RunShellCommand(['getprop'], check_return=True)
for prop in props:
logger.info(' %s', prop)
@@ -527,7 +553,8 @@ def main(raw_args):
help='Disable the system chrome from devices.')
parser.add_argument(
'--emulators', action='store_true',
- help='provision only emulators and ignore usb devices')
+ 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.')
@@ -544,11 +571,14 @@ def main(raw_args):
' wait after each reboot '
'(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT)
parser.add_argument(
+ '--remove-system-apps', nargs='*', dest='system_app_remove_list',
+ help='the names of system apps to remove')
+ parser.add_argument(
'--remove-system-webview', action='store_true',
help='Remove the system webview from devices.')
parser.add_argument(
'--skip-wipe', action='store_true', default=False,
- help="don't wipe device data during provisioning")
+ help='do not wipe device data during provisioning')
parser.add_argument(
'-v', '--verbose', action='count', default=1,
help='Log more information.')
@@ -596,7 +626,8 @@ def main(raw_args):
output_device_blacklist=args.output_device_blacklist,
reboot_timeout=args.reboot_timeout,
remove_system_webview=args.remove_system_webview,
- wipe=not args.skip_wipe)
+ system_app_remove_list=args.system_app_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
diff --git a/catapult/devil/devil/android/tools/video_recorder.py b/catapult/devil/devil/android/tools/video_recorder.py
index 28def21a..a91e6496 100755
--- a/catapult/devil/devil/android/tools/video_recorder.py
+++ b/catapult/devil/devil/android/tools/video_recorder.py
@@ -107,7 +107,7 @@ class VideoRecorder(object):
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.RunShellCommand('rm -f "%s"' % self._device_file)
+ self._device.RemovePath(self._device_file, force=True)
return host_file_name
diff --git a/catapult/devil/devil/utils/cmd_helper.py b/catapult/devil/devil/utils/cmd_helper.py
index 269be5bb..06c105fc 100644
--- a/catapult/devil/devil/utils/cmd_helper.py
+++ b/catapult/devil/devil/utils/cmd_helper.py
@@ -12,6 +12,7 @@ import signal
import string
import StringIO
import subprocess
+import sys
import time
# fcntl is not available on Windows.
@@ -93,10 +94,15 @@ def ShrinkToSnippet(cmd_parts, var_name, var_value):
def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
+ # preexec_fn isn't supported on windows.
+ if sys.platform == 'win32':
+ preexec_fn = None
+ else:
+ 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=True, env=env,
- preexec_fn=lambda: signal.signal(signal.SIGPIPE, signal.SIG_DFL))
+ shell=shell, close_fds=True, env=env, preexec_fn=preexec_fn)
def Call(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
@@ -213,8 +219,30 @@ class TimeoutError(Exception):
return self._output
-def _IterProcessStdout(process, timeout=None, buffer_size=4096,
- poll_interval=1):
+def _IterProcessStdout(process, iter_timeout=None, timeout=None,
+ buffer_size=4096, poll_interval=1):
+ """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.
+ """
+
assert fcntl, 'fcntl module is required'
try:
# Enable non-blocking reads from the child's stdout.
@@ -223,10 +251,24 @@ def _IterProcessStdout(process, timeout=None, buffer_size=4096,
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()
- read_fds, _, _ = select.select([child_fd], [], [], poll_interval)
+ 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:
@@ -283,20 +325,21 @@ def GetCmdStatusAndOutputWithTimeout(args, timeout, cwd=None, shell=False,
return process.returncode, str_output
-def IterCmdOutputLines(args, timeout=None, cwd=None, shell=False,
- check_status=True):
+def IterCmdOutputLines(args, iter_timeout=None, timeout=None, cwd=None,
+ shell=False, check_status=True):
"""Executes a subprocess and continuously yields lines from its output.
Args:
args: List of arguments to the program, the program to execute is the first
element.
+ 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.
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.
@@ -307,14 +350,44 @@ def IterCmdOutputLines(args, timeout=None, cwd=None, shell=False,
cmd = _ValidateAndLogCommand(args, cwd, shell)
process = Popen(args, cwd=cwd, shell=shell, 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 = ''
- for data in _IterProcessStdout(process, timeout=timeout):
+
+ 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:
diff --git a/catapult/devil/devil/utils/cmd_helper_test.py b/catapult/devil/devil/utils/cmd_helper_test.py
index a04f1adf..783c4137 100755
--- a/catapult/devil/devil/utils/cmd_helper_test.py
+++ b/catapult/devil/devil/utils/cmd_helper_test.py
@@ -7,9 +7,14 @@
import unittest
import subprocess
+import time
+from devil import devil_env
from devil.utils import cmd_helper
+with devil_env.SysPath(devil_env.PYMOCK_PATH):
+ import mock # pylint: disable=import-error
+
class CmdHelperSingleQuoteTest(unittest.TestCase):
@@ -84,35 +89,173 @@ class CmdHelperShinkToSnippetTest(unittest.TestCase):
cmd_helper.ShrinkToSnippet(['foo', ' barbar '], 'a', 'bar'))
+_DEFAULT = 'DEFAULT'
+
+
+class _ProcessOutputEvent(object):
+
+ def __init__(self, select_fds=_DEFAULT, read_contents=None, ts=_DEFAULT):
+ self.select_fds = select_fds
+ self.read_contents = read_contents
+ self.ts = ts
+
+
+class _MockProcess(object):
+
+ def __init__(self, output_sequence=None, return_value=0):
+
+ # Arbitrary.
+ fake_stdout_fileno = 25
+
+ self.mock_proc = mock.MagicMock(spec=subprocess.Popen)
+ self.mock_proc.stdout = mock.MagicMock()
+ self.mock_proc.stdout.fileno = mock.MagicMock(
+ return_value=fake_stdout_fileno)
+ self.mock_proc.returncode = None
+
+ self._return_value = return_value
+
+ # This links the behavior of os.read, select.select, time.time, and
+ # <process>.poll. The output sequence can be thought of as a list of
+ # return values for select.select with corresponding return values for
+ # the other calls at any time between that select call and the following
+ # one. We iterate through the sequence only on calls to select.select.
+ #
+ # os.read is a special case, though, where we only return a given chunk
+ # of data *once* after a given call to select.
+
+ if not output_sequence:
+ output_sequence = []
+
+ # Use an leading element to make the iteration logic work.
+ initial_seq_element = _ProcessOutputEvent(
+ _DEFAULT, '',
+ output_sequence[0].ts if output_sequence else _DEFAULT)
+ output_sequence.insert(0, initial_seq_element)
+
+ for o in output_sequence:
+ if o.select_fds == _DEFAULT:
+ if o.read_contents is None:
+ o.select_fds = []
+ else:
+ o.select_fds = [fake_stdout_fileno]
+ if o.ts == _DEFAULT:
+ o.ts = time.time()
+ self._output_sequence = output_sequence
+
+ self._output_seq_index = 0
+ self._read_flags = [False] * len(output_sequence)
+
+ def read_side_effect(*_args, **_kwargs):
+ if self._read_flags[self._output_seq_index]:
+ return None
+ self._read_flags[self._output_seq_index] = True
+ return self._output_sequence[self._output_seq_index].read_contents
+
+ def select_side_effect(*_args, **_kwargs):
+ if self._output_seq_index is None:
+ self._output_seq_index = 0
+ else:
+ self._output_seq_index += 1
+ return (self._output_sequence[self._output_seq_index].select_fds,
+ None, None)
+
+ def time_side_effect(*_args, **_kwargs):
+ return self._output_sequence[self._output_seq_index].ts
+
+ def poll_side_effect(*_args, **_kwargs):
+ if self._output_seq_index >= len(self._output_sequence) - 1:
+ self.mock_proc.returncode = self._return_value
+ return self.mock_proc.returncode
+
+ mock_read = mock.MagicMock(side_effect=read_side_effect)
+ mock_select = mock.MagicMock(side_effect=select_side_effect)
+ mock_time = mock.MagicMock(side_effect=time_side_effect)
+ self.mock_proc.poll = mock.MagicMock(side_effect=poll_side_effect)
+
+ # Set up but *do not start* the mocks.
+ self._mocks = [
+ mock.patch('fcntl.fcntl'),
+ mock.patch('os.read', new=mock_read),
+ mock.patch('select.select', new=mock_select),
+ mock.patch('time.time', new=mock_time),
+ ]
+
+ def __enter__(self):
+ for m in self._mocks:
+ m.__enter__()
+ return self.mock_proc
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ for m in reversed(self._mocks):
+ m.__exit__(exc_type, exc_val, exc_tb)
+
+
class CmdHelperIterCmdOutputLinesTest(unittest.TestCase):
"""Test IterCmdOutputLines with some calls to the unix 'seq' command."""
+ # This calls _IterCmdOutputLines rather than IterCmdOutputLines s.t. it
+ # can mock the process.
+ # pylint: disable=protected-access
+
+ _SIMPLE_OUTPUT_SEQUENCE = [
+ _ProcessOutputEvent(read_contents='1\n2\n'),
+ ]
+
def testIterCmdOutputLines_success(self):
- for num, line in enumerate(
- cmd_helper.IterCmdOutputLines(['seq', '10']), 1):
- self.assertEquals(num, int(line))
+ with _MockProcess(
+ output_sequence=self._SIMPLE_OUTPUT_SEQUENCE) as mock_proc:
+ for num, line in enumerate(
+ cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc'), 1):
+ self.assertEquals(num, int(line))
def testIterCmdOutputLines_exitStatusFail(self):
with self.assertRaises(subprocess.CalledProcessError):
- for num, line in enumerate(
- cmd_helper.IterCmdOutputLines('seq 10 && false', shell=True), 1):
- self.assertEquals(num, int(line))
- # after reading all the output we get an exit status of 1
+ with _MockProcess(output_sequence=self._SIMPLE_OUTPUT_SEQUENCE,
+ return_value=1) as mock_proc:
+ for num, line in enumerate(
+ cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc'), 1):
+ self.assertEquals(num, int(line))
+ # after reading all the output we get an exit status of 1
def testIterCmdOutputLines_exitStatusIgnored(self):
- for num, line in enumerate(
- cmd_helper.IterCmdOutputLines('seq 10 && false', shell=True,
- check_status=False), 1):
- self.assertEquals(num, int(line))
+ with _MockProcess(output_sequence=self._SIMPLE_OUTPUT_SEQUENCE,
+ return_value=1) as mock_proc:
+ for num, line in enumerate(
+ cmd_helper._IterCmdOutputLines(
+ mock_proc, 'mock_proc', check_status=False),
+ 1):
+ self.assertEquals(num, int(line))
def testIterCmdOutputLines_exitStatusSkipped(self):
- for num, line in enumerate(
- cmd_helper.IterCmdOutputLines('seq 10 && false', shell=True), 1):
- self.assertEquals(num, int(line))
- # no exception will be raised because we don't attempt to read past
- # the end of the output and, thus, the status never gets checked
- if num == 10:
- break
+ with _MockProcess(output_sequence=self._SIMPLE_OUTPUT_SEQUENCE,
+ return_value=1) as mock_proc:
+ for num, line in enumerate(
+ cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc'), 1):
+ self.assertEquals(num, int(line))
+ # no exception will be raised because we don't attempt to read past
+ # the end of the output and, thus, the status never gets checked
+ if num == 2:
+ break
+
+ def testIterCmdOutputLines_delay(self):
+ output_sequence = [
+ _ProcessOutputEvent(read_contents='1\n2\n', ts=1),
+ _ProcessOutputEvent(read_contents=None, ts=2),
+ _ProcessOutputEvent(read_contents='Awake', ts=10),
+ ]
+ with _MockProcess(output_sequence=output_sequence) as mock_proc:
+ for num, line in enumerate(
+ cmd_helper._IterCmdOutputLines(mock_proc, 'mock_proc',
+ iter_timeout=5), 1):
+ if num <= 2:
+ self.assertEquals(num, int(line))
+ elif num == 3:
+ self.assertEquals(None, line)
+ elif num == 4:
+ self.assertEquals('Awake', line)
+ else:
+ self.fail()
if __name__ == '__main__':
diff --git a/catapult/devil/devil/utils/markdown.py b/catapult/devil/devil/utils/markdown.py
index cb2dc2bd..54e7ed56 100755
--- a/catapult/devil/devil/utils/markdown.py
+++ b/catapult/devil/devil/utils/markdown.py
@@ -1,8 +1,69 @@
+#! /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 argparse
+import imp
+import os
+import re
+import sys
+import textwrap
+import types
+
+# A markdown code block template: https://goo.gl/9EsyRi
+_CODE_BLOCK_FORMAT = '''```{language}
+{code}
+```
+'''
+
+_DEVIL_ROOT = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), '..', '..'))
+
+
+def md_bold(raw_text):
+ """Returns markdown-formatted bold text."""
+ return '**%s**' % md_escape(raw_text, characters='*')
+
+
+def md_code(raw_text, language):
+ """Returns a markdown-formatted code block in the given language."""
+ return _CODE_BLOCK_FORMAT.format(
+ language=language or '',
+ code=md_escape(raw_text, characters='`'))
+
+
+def md_escape(raw_text, characters='*_'):
+ """Escapes * and _."""
+ def escape_char(m):
+ return '\\%s' % m.group(0)
+ pattern = '[%s]' % re.escape(characters)
+ return re.sub(pattern, escape_char, raw_text)
+
+
+def md_heading(raw_text, level):
+ """Returns markdown-formatted heading."""
+ adjusted_level = min(max(level, 0), 6)
+ return '%s%s%s' % (
+ '#' * adjusted_level, ' ' if adjusted_level > 0 else '', raw_text)
+
+
+def md_inline_code(raw_text):
+ """Returns markdown-formatted inline code."""
+ return '`%s`' % md_escape(raw_text, characters='`')
+
+
+def md_italic(raw_text):
+ """Returns markdown-formatted italic text."""
+ return '*%s*' % md_escape(raw_text, characters='*')
+
+
+def md_link(link_text, link_target):
+ """returns a markdown-formatted link."""
+ return '[%s](%s)' % (
+ md_escape(link_text, characters=']'),
+ md_escape(link_target, characters=')'))
+
class MarkdownHelpFormatter(argparse.HelpFormatter):
"""A really bare-bones argparse help formatter that generates valid markdown.
@@ -25,28 +86,25 @@ class MarkdownHelpFormatter(argparse.HelpFormatter):
def _format_usage(self, usage, actions, groups, prefix):
usage_text = super(MarkdownHelpFormatter, self)._format_usage(
usage, actions, groups, prefix)
- return '\n```\n%s\n```\n\n' % usage_text
+ return md_code(usage_text, language=None)
#override
def format_help(self):
- self._root_section.heading = '# %s' % self._prog
+ self._root_section.heading = md_heading(self._prog, level=1)
return super(MarkdownHelpFormatter, self).format_help()
#override
def start_section(self, heading):
- super(MarkdownHelpFormatter, self).start_section('## **%s**' % heading)
+ super(MarkdownHelpFormatter, self).start_section(
+ md_heading(heading, level=2))
#override
def _format_action(self, action):
lines = []
action_header = self._format_action_invocation(action)
- lines.append('### **%s** ' % action_header)
+ lines.append(md_heading(action_header, level=3))
if action.help:
- lines.append('')
- lines.append('```')
- help_text = self._expand_help(action)
- lines.extend(self._split_lines(help_text, 80))
- lines.append('```')
+ lines.append(md_code(self._expand_help(action), language=None))
lines.extend(['', ''])
return '\n'.join(lines)
@@ -69,6 +127,194 @@ class MarkdownHelpAction(argparse.Action):
def add_md_help_argument(parser):
+ """Adds --md-help to the given argparse.ArgumentParser.
+
+ Running a script with --md-help will print the help text for that script
+ as valid markdown.
+
+ Args:
+ parser: The ArgumentParser to which --md-help should be added.
+ """
parser.add_argument('--md-help', action=MarkdownHelpAction,
help='print Markdown-formatted help text and exit.')
+
+def load_module_from_path(module_path):
+ """Load a module given only the path name.
+
+ Also loads package modules as necessary.
+
+ Args:
+ module_path: An absolute path to a python module.
+ Returns:
+ The module object for the given path.
+ """
+ module_names = [os.path.splitext(os.path.basename(module_path))[0]]
+ d = os.path.dirname(module_path)
+
+ while os.path.exists(os.path.join(d, '__init__.py')):
+ module_names.append(os.path.basename(d))
+ d = os.path.dirname(d)
+
+ d = [d]
+
+ module = None
+ full_module_name = ''
+ for package_name in reversed(module_names):
+ if module:
+ d = module.__path__
+ full_module_name += '.'
+ r = imp.find_module(package_name, d)
+ full_module_name += package_name
+ module = imp.load_module(full_module_name, *r)
+ return module
+
+
+def md_module(module_obj, module_path=None, module_link=None):
+ """Write markdown documentation for a class.
+
+ Documents public classes and functions.
+
+ Args:
+ class_obj: a types.TypeType object for the class that should be
+ documented.
+ Returns:
+ A list of markdown-formatted lines.
+ """
+ def should_doc(name):
+ return (type(module_obj.__dict__[name]) != types.ModuleType
+ and not name.startswith('_'))
+
+ stuff_to_doc = sorted(
+ obj for name, obj in module_obj.__dict__.iteritems()
+ if should_doc(name))
+
+ classes_to_doc = []
+ functions_to_doc = []
+
+ for s in stuff_to_doc:
+ if type(s) == types.TypeType:
+ classes_to_doc.append(s)
+ elif type(s) == types.FunctionType:
+ functions_to_doc.append(s)
+
+ command = ['devil/utils/markdown.py']
+ if module_link:
+ command.extend(['--module-link', module_link])
+ if module_path:
+ command.append(os.path.relpath(module_path, _DEVIL_ROOT))
+
+ heading_text = module_obj.__name__
+ if module_link:
+ heading_text = md_link(heading_text, module_link)
+
+ content = [
+ md_heading(heading_text, level=1),
+ '',
+ md_italic('This page was autogenerated by %s'
+ % md_inline_code(' '.join(command))),
+ '',
+ ]
+
+ for c in classes_to_doc:
+ content += md_class(c)
+ for f in functions_to_doc:
+ content += md_function(f)
+
+ print '\n'.join(content)
+
+ return 0
+
+
+def md_class(class_obj):
+ """Write markdown documentation for a class.
+
+ Documents public methods. Does not currently document subclasses.
+
+ Args:
+ class_obj: a types.TypeType object for the class that should be
+ documented.
+ Returns:
+ A list of markdown-formatted lines.
+ """
+ content = [md_heading(md_escape(class_obj.__name__), level=2)]
+ content.append('')
+ if class_obj.__doc__:
+ content.extend(md_docstring(class_obj.__doc__))
+
+ def should_doc(name, obj):
+ return (type(obj) == types.FunctionType
+ and (name.startswith('__') or not name.startswith('_')))
+
+ methods_to_doc = sorted(
+ obj for name, obj in class_obj.__dict__.iteritems()
+ if should_doc(name, obj))
+
+ for m in methods_to_doc:
+ content.extend(md_function(m, class_obj=class_obj))
+
+ return content
+
+
+def md_docstring(docstring):
+ """Write a markdown-formatted docstring.
+
+ Returns:
+ A list of markdown-formatted lines.
+ """
+ content = []
+ lines = textwrap.dedent(docstring).splitlines()
+ content.append(md_escape(lines[0]))
+ lines = lines[1:]
+ while lines and (not lines[0] or lines[0].isspace()):
+ lines = lines[1:]
+
+ if not all(l.isspace() for l in lines):
+ content.append(md_code('\n'.join(lines), language=None))
+ content.append('')
+ return content
+
+
+def md_function(func_obj, class_obj=None):
+ """Write markdown documentation for a function.
+
+ Args:
+ func_obj: a types.FunctionType object for the function that should be
+ documented.
+ Returns:
+ A list of markdown-formatted lines.
+ """
+ if class_obj:
+ heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__)
+ else:
+ heading_text = func_obj.__name__
+ content = [md_heading(md_escape(heading_text), level=3)]
+ content.append('')
+
+ if func_obj.__doc__:
+ content.extend(md_docstring(func_obj.__doc__))
+
+ return content
+
+
+def main(raw_args):
+ """Write markdown documentation for the module at the provided path.
+
+ Args:
+ raw_args: the raw command-line args. Usually sys.argv[1:].
+ Returns:
+ An integer exit code. 0 for success, non-zero for failure.
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--module-link')
+ parser.add_argument('module_path', type=os.path.realpath)
+ args = parser.parse_args(raw_args)
+
+ return md_module(
+ load_module_from_path(args.module_path),
+ module_link=args.module_link)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
+
diff --git a/catapult/devil/devil/utils/markdown_test.py b/catapult/devil/devil/utils/markdown_test.py
new file mode 100755
index 00000000..323776ca
--- /dev/null
+++ b/catapult/devil/devil/utils/markdown_test.py
@@ -0,0 +1,121 @@
+#! /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 textwrap
+import unittest
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
+
+from devil.utils import markdown
+
+
+class MarkdownTest(unittest.TestCase):
+
+ def testBold(self):
+ raw = 'foo'
+ self.assertEquals('**foo**', markdown.md_bold(raw))
+
+ def testBoldContainsStars(self):
+ raw = '*foo*'
+ self.assertEquals('**\\*foo\\***', markdown.md_bold(raw))
+
+ def testCode(self):
+ raw = textwrap.dedent("""\
+ class MarkdownTest(unittest.TestCase):
+ def testCode(self):
+ pass""")
+
+ expected = textwrap.dedent("""\
+ ```python
+ class MarkdownTest(unittest.TestCase):
+ def testCode(self):
+ pass
+ ```
+ """)
+ actual = markdown.md_code(raw, language='python')
+ self.assertEquals(expected, actual)
+
+ def testCodeContainsTicks(self):
+ raw = textwrap.dedent("""\
+ This is sample markdown.
+ ```c
+ // This is a sample code block.
+ int main(int argc, char** argv) {
+ return 0;
+ }
+ ```""")
+
+ expected = textwrap.dedent("""\
+ ```
+ This is sample markdown.
+ \\`\\`\\`c
+ // This is a sample code block.
+ int main(int argc, char** argv) {
+ return 0;
+ }
+ \\`\\`\\`
+ ```
+ """)
+ actual = markdown.md_code(raw, language=None)
+ self.assertEquals(expected, actual)
+
+ def testEscape(self):
+ raw = 'text_with_underscores *and stars*'
+ expected = 'text\\_with\\_underscores \\*and stars\\*'
+ actual = markdown.md_escape(raw)
+ self.assertEquals(expected, actual)
+
+ def testHeading1(self):
+ raw = 'Heading 1'
+ self.assertEquals('# Heading 1', markdown.md_heading(raw, level=1))
+
+ def testHeading5(self):
+ raw = 'Heading 5'
+ self.assertEquals('##### Heading 5', markdown.md_heading(raw, level=5))
+
+ def testHeading10(self):
+ raw = 'Heading 10'
+ self.assertEquals('###### Heading 10', markdown.md_heading(raw, level=10))
+
+ def testInlineCode(self):
+ raw = 'devil.utils.markdown_test'
+ self.assertEquals(
+ '`devil.utils.markdown_test`', markdown.md_inline_code(raw))
+
+ def testInlineCodeContainsTicks(self):
+ raw = 'this contains `backticks`'
+ self.assertEquals(
+ '`this contains \\`backticks\\``', markdown.md_inline_code(raw))
+
+ def testItalic(self):
+ raw = 'bar'
+ self.assertEquals('*bar*', markdown.md_italic(raw))
+
+ def testItalicContainsStars(self):
+ raw = '*bar*'
+ self.assertEquals('*\\*bar\\**', markdown.md_italic(raw))
+
+ def testLink(self):
+ link_text = 'Devil home'
+ link_target = (
+ 'https://github.com/catapult-project/catapult/tree/master/devil')
+ expected = (
+ '[Devil home]'
+ '(https://github.com/catapult-project/catapult/tree/master/devil)')
+ self.assertEquals(expected, markdown.md_link(link_text, link_target))
+
+ def testLinkTextContainsBracket(self):
+ link_text = 'foo [] bar'
+ link_target = 'https://www.google.com'
+ expected = '[foo [\\] bar](https://www.google.com)'
+ self.assertEquals(expected, markdown.md_link(link_text, link_target))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/catapult/devil/docs/adb_wrapper.md b/catapult/devil/docs/adb_wrapper.md
new file mode 100644
index 00000000..a8dc3b05
--- /dev/null
+++ b/catapult/devil/docs/adb_wrapper.md
@@ -0,0 +1,388 @@
+# [devil.android.sdk.adb_wrapper](https://github.com/catapult-project/catapult/blob/master/devil/devil/android/sdk/adb_wrapper.py)
+
+*This page was autogenerated by `devil/utils/markdown.py --module-link https://github.com/catapult-project/catapult/blob/master/devil/devil/android/sdk/adb_wrapper.py`*
+
+## DeviceStat
+
+DeviceStat(st\_mode, st\_size, st\_time)
+### DeviceStat.\_\_repr\_\_
+
+Return a nicely formatted representation string
+### DeviceStat.\_\_getnewargs\_\_
+
+Return self as a plain tuple. Used by copy and pickle.
+### DeviceStat.\_\_getstate\_\_
+
+Exclude the OrderedDict from pickling
+## AdbWrapper
+
+A wrapper around a local Android Debug Bridge executable.
+### AdbWrapper.GetDeviceSerial
+
+Gets the device serial number associated with this object.
+```
+ Returns:
+ Device serial number as a string.
+```
+
+
+### AdbWrapper.Push
+
+Pushes a file from the host to the device.
+```
+ Args:
+ local: Path on the host filesystem.
+ remote: Path on the device filesystem.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Pull
+
+Pulls a file from the device to the host.
+```
+ Args:
+ remote: Path on the device filesystem.
+ local: Path on the host filesystem.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Shell
+
+Runs a shell command on the device.
+```
+ Args:
+ command: A string with the shell command to run.
+ expect_status: (optional) Check that the command's exit status matches
+ this value. Default is 0. If set to None the test is skipped.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+
+ Returns:
+ The output of the shell command as a string.
+
+ Raises:
+ device_errors.AdbCommandFailedError: If the exit status doesn't match
+ |expect_status|.
+```
+
+
+### AdbWrapper.IterShell
+
+Runs a shell command and returns an iterator over its output lines.
+```
+ Args:
+ command: A string with the shell command to run.
+ timeout: Timeout in seconds.
+
+ Yields:
+ The output of the command line by line.
+```
+
+
+### AdbWrapper.Ls
+
+List the contents of a directory on the device.
+```
+ Args:
+ path: Path on the device filesystem.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+
+ Returns:
+ A list of pairs (filename, stat) for each file found in the directory,
+ where the stat object has the properties: st_mode, st_size, and st_time.
+
+ Raises:
+ AdbCommandFailedError if |path| does not specify a valid and accessible
+ directory in the device, or the output of "adb ls" command is less
+ than four columns
+```
+
+
+### AdbWrapper.Logcat
+
+Get an iterable over the logcat output.
+```
+ Args:
+ clear: If true, clear the logcat.
+ dump: If true, dump the current logcat contents.
+ filter_specs: If set, a list of specs to filter the logcat.
+ logcat_format: If set, the format in which the logcat should be output.
+ Options include "brief", "process", "tag", "thread", "raw", "time",
+ "threadtime", and "long"
+ ring_buffer: If set, a list of alternate ring buffers to request.
+ Options include "main", "system", "radio", "events", "crash" or "all".
+ The default is equivalent to ["main", "system", "crash"].
+ iter_timeout: If set and neither clear nor dump is set, the number of
+ seconds to wait between iterations. If no line is found before the
+ given number of seconds elapses, the iterable will yield None.
+ timeout: (optional) If set, timeout per try in seconds. If clear or dump
+ is set, defaults to DEFAULT_TIMEOUT.
+ retries: (optional) If clear or dump is set, the number of retries to
+ attempt. Otherwise, does nothing.
+
+ Yields:
+ logcat output line by line.
+```
+
+
+### AdbWrapper.Forward
+
+Forward socket connections from the local socket to the remote socket.
+```
+ Sockets are specified by one of:
+ tcp:<port>
+ localabstract:<unix domain socket name>
+ localreserved:<unix domain socket name>
+ localfilesystem:<unix domain socket name>
+ dev:<character device name>
+ jdwp:<process pid> (remote only)
+
+ Args:
+ local: The host socket.
+ remote: The device socket.
+ allow_rebind: A boolean indicating whether adb may rebind a local socket;
+ otherwise, the default, an exception is raised if the local socket is
+ already being forwarded.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.ForwardRemove
+
+Remove a forward socket connection.
+```
+ Args:
+ local: The host socket.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.ForwardList
+
+List all currently forwarded socket connections.
+```
+ Args:
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+ Returns:
+ The output of adb forward --list as a string.
+```
+
+
+### AdbWrapper.JDWP
+
+List of PIDs of processes hosting a JDWP transport.
+```
+ Args:
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+
+ Returns:
+ A list of PIDs as strings.
+```
+
+
+### AdbWrapper.Install
+
+Install an apk on the device.
+```
+ Args:
+ apk_path: Host path to the APK file.
+ forward_lock: (optional) If set forward-locks the app.
+ allow_downgrade: (optional) If set, allows for downgrades.
+ reinstall: (optional) If set reinstalls the app, keeping its data.
+ sd_card: (optional) If set installs on the SD card.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.InstallMultiple
+
+Install an apk with splits on the device.
+```
+ Args:
+ apk_paths: Host path to the APK file.
+ forward_lock: (optional) If set forward-locks the app.
+ reinstall: (optional) If set reinstalls the app, keeping its data.
+ sd_card: (optional) If set installs on the SD card.
+ allow_downgrade: (optional) Allow versionCode downgrade.
+ partial: (optional) Package ID if apk_paths doesn't include all .apks.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Uninstall
+
+Remove the app |package| from the device.
+```
+ Args:
+ package: The package to uninstall.
+ keep_data: (optional) If set keep the data and cache directories.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Backup
+
+Write an archive of the device's data to |path|.
+```
+ Args:
+ path: Local path to store the backup file.
+ packages: List of to packages to be backed up.
+ apk: (optional) If set include the .apk files in the archive.
+ shared: (optional) If set buckup the device's SD card.
+ nosystem: (optional) If set exclude system applications.
+ include_all: (optional) If set back up all installed applications and
+ |packages| is optional.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Restore
+
+Restore device contents from the backup archive.
+```
+ Args:
+ path: Host path to the backup archive.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.WaitForDevice
+
+Block until the device is online.
+```
+ Args:
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.GetState
+
+Get device state.
+```
+ Args:
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+
+ Returns:
+ One of 'offline', 'bootloader', or 'device'.
+```
+
+
+### AdbWrapper.GetDevPath
+
+Gets the device path.
+```
+ Args:
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+
+ Returns:
+ The device path (e.g. usb:3-4)
+```
+
+
+### AdbWrapper.Remount
+
+Remounts the /system partition on the device read-write.
+### AdbWrapper.Reboot
+
+Reboots the device.
+```
+ Args:
+ to_bootloader: (optional) If set reboots to the bootloader.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Root
+
+Restarts the adbd daemon with root permissions, if possible.
+```
+ Args:
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+```
+
+
+### AdbWrapper.Emu
+
+Runs an emulator console command.
+```
+ See http://developer.android.com/tools/devices/emulator.html#console
+
+ Args:
+ cmd: The command to run on the emulator console.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+
+ Returns:
+ The output of the emulator console command.
+```
+
+
+### AdbWrapper.DisableVerity
+
+Disable Marshmallow's Verity security feature
+### AdbWrapper.EnableVerity
+
+Enable Marshmallow's Verity security feature
+### AdbWrapper.\_\_init\_\_
+
+Initializes the AdbWrapper.
+```
+ Args:
+ device_serial: The device serial number as a string.
+```
+
+
+### AdbWrapper.\_\_eq\_\_
+
+Consider instances equal if they refer to the same device.
+```
+ Args:
+ other: The instance to compare equality with.
+
+ Returns:
+ True if the instances are considered equal, false otherwise.
+```
+
+
+### AdbWrapper.\_\_str\_\_
+
+The string representation of an instance.
+```
+ Returns:
+ The device serial number as a string.
+```
+
+
+### AdbWrapper.\_\_repr\_\_
+
+### VerifyLocalFileExists
+
+Verifies a local file exists.
+```
+ Args:
+ path: Path to the local file.
+
+ Raises:
+ IOError: If the file doesn't exist.
+```
+
+
diff --git a/catapult/devil/docs/device_utils.md b/catapult/devil/docs/device_utils.md
new file mode 100644
index 00000000..b281b266
--- /dev/null
+++ b/catapult/devil/docs/device_utils.md
@@ -0,0 +1,1041 @@
+# [devil.android.device_utils](https://github.com/catapult-project/catapult/blob/master/devil/devil/android/device_utils.py)
+
+*This page was autogenerated by `devil/utils/markdown.py --module-link https://github.com/catapult-project/catapult/blob/master/devil/devil/android/device_utils.py`*
+
+## DeviceUtils
+
+### DeviceUtils.\_\_init\_\_
+
+DeviceUtils constructor.
+```
+ Args:
+ device: Either a device serial, an existing AdbWrapper instance, or an
+ an existing AndroidCommands instance.
+ enable_device_files_cache: For PushChangedFiles(), cache checksums of
+ pushed files rather than recomputing them on a subsequent call.
+ 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.
+```
+
+
+### DeviceUtils.\_\_eq\_\_
+
+Checks whether |other| refers to the same device as |self|.
+```
+ Args:
+ other: The object to compare to. This can be a basestring, an instance
+ of adb_wrapper.AdbWrapper, or an instance of DeviceUtils.
+ Returns:
+ Whether |other| refers to the same device as |self|.
+```
+
+
+### DeviceUtils.\_\_lt\_\_
+
+Compares two instances of DeviceUtils.
+```
+ This merely compares their serial numbers.
+
+ Args:
+ other: The instance of DeviceUtils to compare to.
+ Returns:
+ Whether |self| is less than |other|.
+```
+
+
+### DeviceUtils.\_\_str\_\_
+
+Returns the device serial.
+### DeviceUtils.NeedsSU
+
+Checks whether 'su' is needed to access protected resources.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if 'su' is available on the device and is needed to to access
+ protected resources; False otherwise if either 'su' is not available
+ (e.g. because the device has a user build), or not needed (because adbd
+ already has root privileges).
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.IsOnline
+
+Checks whether the device is online.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if the device is online, False otherwise.
+
+ Raises:
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.HasRoot
+
+Checks whether or not adbd has root privileges.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if adbd has root privileges, False otherwise.
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.EnableRoot
+
+Restarts adbd with root privileges.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if root could not be enabled.
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.IsUserBuild
+
+Checks whether or not the device is running a user build.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if the device is running a user build, False otherwise (i.e. if
+ it's running a userdebug build).
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.GetExternalStoragePath
+
+Get the device's path to its SD card.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The device's path to its SD card.
+
+ Raises:
+ CommandFailedError if the external storage path could not be determined.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.GetApplicationPaths
+
+Get the paths of the installed apks on the device for the given package.
+```
+ Args:
+ package: Name of the package.
+
+ Returns:
+ List of paths to the apks on the device for the given package.
+```
+
+
+### DeviceUtils.GetApplicationVersion
+
+Get the version name of a package installed on the device.
+```
+ Args:
+ package: Name of the package.
+
+ Returns:
+ A string with the version name or None if the package is not found
+ on the device.
+```
+
+
+### DeviceUtils.GetApplicationDataDirectory
+
+Get the data directory on the device for the given package.
+```
+ Args:
+ package: Name of the package.
+
+ Returns:
+ The package's data directory.
+ Raises:
+ CommandFailedError if the package's data directory can't be found,
+ whether because it's not installed or otherwise.
+```
+
+
+### DeviceUtils.WaitUntilFullyBooted
+
+Wait for the device to fully boot.
+```
+ This means waiting for the device to boot, the package manager to be
+ available, and the SD card to be ready. It can optionally mean waiting
+ for wifi to come up, too.
+
+ Args:
+ wifi: A boolean indicating if we should wait for wifi to come up or not.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError if one of the component waits times out.
+ DeviceUnreachableError if the device becomes unresponsive.
+```
+
+
+### DeviceUtils.Reboot
+
+Reboot the device.
+```
+ Args:
+ block: A boolean indicating if we should wait for the reboot to complete.
+ wifi: A boolean indicating if we should wait for wifi to be enabled after
+ the reboot. The option has no effect unless |block| is also True.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.Install
+
+Install an APK.
+```
+ Noop if an identical APK is already installed.
+
+ Args:
+ apk: An ApkHelper instance or string containing the path to the APK.
+ allow_downgrade: A boolean indicating if we should allow downgrades.
+ reinstall: A boolean indicating if we should keep any existing app data.
+ permissions: Set of permissions to set. If not set, finds permissions with
+ apk helper. To set no permissions, pass [].
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if the installation fails.
+ CommandTimeoutError if the installation times out.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.InstallSplitApk
+
+Install a split APK.
+```
+ Noop if all of the APK splits are already installed.
+
+ Args:
+ base_apk: An ApkHelper instance or string containing the path to the base
+ APK.
+ split_apks: A list of strings of paths of all of the APK splits.
+ allow_downgrade: A boolean indicating if we should allow downgrades.
+ reinstall: A boolean indicating if we should keep any existing app data.
+ allow_cached_props: Whether to use cached values for device properties.
+ permissions: Set of permissions to set. If not set, finds permissions with
+ apk helper. To set no permissions, pass [].
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if the installation fails.
+ CommandTimeoutError if the installation times out.
+ DeviceUnreachableError on missing device.
+ DeviceVersionError if device SDK is less than Android L.
+```
+
+
+### DeviceUtils.Uninstall
+
+Remove the app |package\_name| from the device.
+```
+ This is a no-op if the app is not already installed.
+
+ Args:
+ package_name: The package to uninstall.
+ keep_data: (optional) Whether to keep the data and cache directories.
+ timeout: Timeout in seconds.
+ retries: Number of retries.
+
+ Raises:
+ CommandFailedError if the uninstallation fails.
+ CommandTimeoutError if the uninstallation times out.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.RunShellCommand
+
+Run an ADB shell command.
+```
+ The command to run |cmd| should be a sequence of program arguments or else
+ a single string.
+
+ When |cmd| is a sequence, it is assumed to contain the name of the command
+ to run followed by its arguments. In this case, arguments are passed to the
+ command exactly as given, without any further processing by the shell. This
+ allows to easily pass arguments containing spaces or special characters
+ without having to worry about getting quoting right. Whenever possible, it
+ is recomended to pass |cmd| as a sequence.
+
+ When |cmd| is given as a string, it will be interpreted and run by the
+ shell on the device.
+
+ This behaviour is consistent with that of command runners in cmd_helper as
+ well as Python's own subprocess.Popen.
+
+ TODO(perezju) Change the default of |check_return| to True when callers
+ have switched to the new behaviour.
+
+ Args:
+ cmd: A string with the full command to run on the device, or a sequence
+ containing the command and its arguments.
+ check_return: A boolean indicating whether or not the return code should
+ be checked.
+ cwd: The device directory in which the command should be run.
+ env: The environment variables with which the command should be run.
+ run_as: A string containing the package as which the command should be
+ run.
+ as_root: A boolean indicating whether the shell command should be run
+ with root privileges.
+ single_line: A boolean indicating if only a single line of output is
+ expected.
+ large_output: Uses a work-around for large shell command output. Without
+ this large output will be truncated.
+ raw_output: Whether to only return the raw output
+ (no splitting into lines).
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ If single_line is False, the output of the command as a list of lines,
+ otherwise, a string with the unique line of output emmited by the command
+ (with the optional newline at the end stripped).
+
+ Raises:
+ AdbCommandFailedError if check_return is True and the exit code of
+ the command run on the device is non-zero.
+ CommandFailedError if single_line is True but the output contains two or
+ more lines.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.KillAll
+
+Kill all processes with the given name on the device.
+```
+ Args:
+ process_name: A string containing the name of the process to kill.
+ exact: A boolean indicating whether to kill all processes matching
+ the string |process_name| exactly, or all of those which contain
+ |process_name| as a substring. Defaults to False.
+ signum: An integer containing the signal number to send to kill. Defaults
+ to SIGKILL (9).
+ as_root: A boolean indicating whether the kill should be executed with
+ root privileges.
+ blocking: A boolean indicating whether we should wait until all processes
+ with the given |process_name| are dead.
+ quiet: A boolean indicating whether to ignore the fact that no processes
+ to kill were found.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The number of processes attempted to kill.
+
+ Raises:
+ CommandFailedError if no process was killed and |quiet| is False.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.StartActivity
+
+Start package's activity on the device.
+```
+ Args:
+ intent_obj: An Intent object to send.
+ blocking: A boolean indicating whether we should wait for the activity to
+ finish launching.
+ trace_file_name: If present, a string that both indicates that we want to
+ profile the activity and contains the path to which the
+ trace should be saved.
+ force_stop: A boolean indicating whether we should stop the activity
+ before starting it.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if the activity could not be started.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.StartInstrumentation
+
+### DeviceUtils.BroadcastIntent
+
+Send a broadcast intent.
+```
+ Args:
+ intent: An Intent to broadcast.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.GoHome
+
+Return to the home screen and obtain launcher focus.
+```
+ This command launches the home screen and attempts to obtain
+ launcher focus until the timeout is reached.
+
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.ForceStop
+
+Close the application.
+```
+ Args:
+ package: A string containing the name of the package to stop.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.ClearApplicationState
+
+Clear all state for the given package.
+```
+ Args:
+ package: A string containing the name of the package to stop.
+ permissions: List of permissions to set after clearing data.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.SendKeyEvent
+
+Sends a keycode to the device.
+```
+ See the devil.android.sdk.keyevent module for suitable keycode values.
+
+ Args:
+ keycode: A integer keycode to send to the device.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.PushChangedFiles
+
+Push files to the device, skipping files that don't need updating.
+```
+ When a directory is pushed, it is traversed recursively on the host and
+ all files in it are pushed to the device as needed.
+ Additionally, if delete_device_stale option is True,
+ files that exist on the device but don't exist on the host are deleted.
+
+ Args:
+ host_device_tuples: A list of (host_path, device_path) tuples, where
+ |host_path| is an absolute path of a file or directory on the host
+ that should be minimially pushed to the device, and |device_path| is
+ an absolute path of the destination on the device.
+ timeout: timeout in seconds
+ retries: number of retries
+ delete_device_stale: option to delete stale files on device
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.FileExists
+
+Checks whether the given file exists on the device.
+```
+ Arguments are the same as PathExists.
+```
+
+
+### DeviceUtils.PathExists
+
+Checks whether the given path(s) exists on the device.
+```
+ Args:
+ device_path: A string containing the absolute path to the file on the
+ device, or an iterable of paths to check.
+ as_root: Whether root permissions should be use to check for the existence
+ of the given path(s).
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if the all given paths exist on the device, False otherwise.
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.RemovePath
+
+Removes the given path(s) from the device.
+```
+ Args:
+ device_path: A string containing the absolute path to the file on the
+ device, or an iterable of paths to check.
+ force: Whether to remove the path(s) with force (-f).
+ recursive: Whether to remove any directories in the path(s) recursively.
+ as_root: Whether root permissions should be use to remove the given
+ path(s).
+ timeout: timeout in seconds
+ retries: number of retries
+```
+
+
+### DeviceUtils.PullFile
+
+Pull a file from the device.
+```
+ Args:
+ device_path: A string containing the absolute path of the file to pull
+ from the device.
+ host_path: A string containing the absolute path of the destination on
+ the host.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.ReadFile
+
+Reads the contents of a file from the device.
+```
+ Args:
+ device_path: A string containing the absolute path of the file to read
+ from the device.
+ as_root: A boolean indicating whether the read should be executed with
+ root privileges.
+ force_pull: A boolean indicating whether to force the operation to be
+ performed by pulling a file from the device. The default is, when the
+ contents are short, to retrieve the contents using cat instead.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The contents of |device_path| as a string. Contents are intepreted using
+ universal newlines, so the caller will see them encoded as '
+'. Also,
+ all lines will be terminated.
+
+ Raises:
+ AdbCommandFailedError if the file can't be read.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.WriteFile
+
+Writes |contents| to a file on the device.
+```
+ Args:
+ device_path: A string containing the absolute path to the file to write
+ on the device.
+ contents: A string containing the data to write to the device.
+ as_root: A boolean indicating whether the write should be executed with
+ root privileges (if available).
+ force_push: A boolean indicating whether to force the operation to be
+ performed by pushing a file to the device. The default is, when the
+ contents are short, to pass the contents using a shell script instead.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if the file could not be written on the device.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.ListDirectory
+
+List all files on a device directory.
+```
+ Mirroring os.listdir (and most client expectations) the resulting list
+ does not include the special entries '.' and '..' even if they are present
+ in the directory.
+
+ Args:
+ device_path: A string containing the path of the directory on the device
+ to list.
+ as_root: A boolean indicating whether the to use root privileges to list
+ the directory contents.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A list of filenames for all entries contained in the directory.
+
+ Raises:
+ AdbCommandFailedError if |device_path| does not specify a valid and
+ accessible directory in the device.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.StatDirectory
+
+List file and stat info for all entries on a device directory.
+```
+ Implementation notes: this is currently implemented by parsing the output
+ of 'ls -a -l' on the device. Whether possible and convenient, we attempt to
+ make parsing strict and return values mirroring those of the standard |os|
+ and |stat| Python modules.
+
+ Mirroring os.listdir (and most client expectations) the resulting list
+ does not include the special entries '.' and '..' even if they are present
+ in the directory.
+
+ Args:
+ device_path: A string containing the path of the directory on the device
+ to list.
+ as_root: A boolean indicating whether the to use root privileges to list
+ the directory contents.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A list of dictionaries, each containing the following keys:
+ filename: A string with the file name.
+ st_mode: File permissions, use the stat module to interpret these.
+ st_nlink: Number of hard links (may be missing).
+ st_owner: A string with the user name of the owner.
+ st_group: A string with the group name of the owner.
+ st_rdev_pair: Device type as (major, minior) (only if inode device).
+ st_size: Size of file, in bytes (may be missing for non-regular files).
+ st_mtime: Time of most recent modification, in seconds since epoch
+ (although resolution is in minutes).
+ symbolic_link_to: If entry is a symbolic link, path where it points to;
+ missing otherwise.
+
+ Raises:
+ AdbCommandFailedError if |device_path| does not specify a valid and
+ accessible directory in the device.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.StatPath
+
+Get the stat attributes of a file or directory on the device.
+```
+ Args:
+ device_path: A string containing the path of a file or directory from
+ which to get attributes.
+ as_root: A boolean indicating whether the to use root privileges to
+ access the file information.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A dictionary with the stat info collected; see StatDirectory for details.
+
+ Raises:
+ CommandFailedError if device_path cannot be found on the device.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.FileSize
+
+Get the size of a file on the device.
+```
+ Note: This is implemented by parsing the output of the 'ls' command on
+ the device. On some Android versions, when passing a directory or special
+ file, the size is *not* reported and this function will throw an exception.
+
+ Args:
+ device_path: A string containing the path of a file on the device.
+ as_root: A boolean indicating whether the to use root privileges to
+ access the file information.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The size of the file in bytes.
+
+ Raises:
+ CommandFailedError if device_path cannot be found on the device, or
+ its size cannot be determited for some reason.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.GetLanguage
+
+Returns the language setting on the device.
+```
+ Args:
+ cache: Whether to use cached properties when available.
+```
+
+
+### DeviceUtils.SetJavaAsserts
+
+Enables or disables Java asserts.
+```
+ Args:
+ enabled: A boolean indicating whether Java asserts should be enabled
+ or disabled.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True if the device-side property changed and a restart is required as a
+ result, False otherwise.
+
+ Raises:
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.GetCountry
+
+Returns the country setting on the device.
+```
+ Args:
+ cache: Whether to use cached properties when available.
+```
+
+
+### DeviceUtils.GetProp
+
+Gets a property from the device.
+```
+ Args:
+ property_name: A string containing the name of the property to get from
+ the device.
+ cache: Whether to use cached properties when available.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The value of the device's |property_name| property.
+
+ Raises:
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.SetProp
+
+Sets a property on the device.
+```
+ Args:
+ property_name: A string containing the name of the property to set on
+ the device.
+ value: A string containing the value to set to the property on the
+ device.
+ check: A boolean indicating whether to check that the property was
+ successfully set on the device.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError if check is true and the property was not correctly
+ set on the device (e.g. because it is not rooted).
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.GetABI
+
+Gets the device main ABI.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The device's main ABI name.
+
+ Raises:
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.GetPids
+
+Returns the PIDs of processes with the given name.
+```
+ Note that the |process_name| is often the package name.
+
+ Args:
+ process_name: A string containing the process name to get the PIDs for.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A dict mapping process name to a list of PIDs for each process that
+ contained the provided |process_name|.
+
+ Raises:
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.GetEnforce
+
+Get the current mode of SELinux.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ True (enforcing), False (permissive), or None (disabled).
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.SetEnforce
+
+Modify the mode SELinux is running in.
+```
+ Args:
+ enabled: a boolean indicating whether to put SELinux in encorcing mode
+ (if True), or permissive mode (otherwise).
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.TakeScreenshot
+
+Takes a screenshot of the device.
+```
+ Args:
+ host_path: A string containing the path on the host to save the
+ screenshot to. If None, a file name in the current
+ directory will be generated.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ The name of the file on the host to which the screenshot was saved.
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+```
+
+
+### DeviceUtils.GetMemoryUsageForPid
+
+Gets the memory usage for the given PID.
+```
+ Args:
+ pid: PID of the process.
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A dict containing memory usage statistics for the PID. May include:
+ Size, Rss, Pss, Shared_Clean, Shared_Dirty, Private_Clean,
+ Private_Dirty, VmHWM
+
+ Raises:
+ CommandTimeoutError on timeout.
+```
+
+
+### DeviceUtils.DismissCrashDialogIfNeeded
+
+Dismiss the error/ANR dialog if present.
+```
+ Returns: Name of the crashed package if a dialog is focused,
+ None otherwise.
+```
+
+
+### DeviceUtils.GetLogcatMonitor
+
+Returns a new LogcatMonitor associated with this device.
+```
+ Parameters passed to this function are passed directly to
+ |logcat_monitor.LogcatMonitor| and are documented there.
+```
+
+
+### DeviceUtils.GetClientCache
+
+Returns client cache.
+### DeviceUtils.LoadCacheData
+
+Initializes the cache from data created using DumpCacheData.
+```
+ The cache is used only if its token matches the one found on the device.
+ This prevents a stale cache from being used (which can happen when sharing
+ devices).
+
+ Args:
+ data: A previously serialized cache (string).
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ Whether the cache was loaded.
+```
+
+
+### DeviceUtils.DumpCacheData
+
+Dumps the current cache state to a string.
+```
+ Args:
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Returns:
+ A serialized cache as a string.
+```
+
+
+### DeviceUtils.RestartAdbd
+
+### DeviceUtils.GrantPermissions
+
+### DeviceUtils.IsScreenOn
+
+Determines if screen is on.
+```
+ Dumpsys input_method exposes screen on/off state. Below is an explination of
+ the states.
+
+ Pre-L:
+ On: mScreenOn=true
+ Off: mScreenOn=false
+ L+:
+ On: mInteractive=true
+ Off: mInteractive=false
+
+ Returns:
+ True if screen is on, false if it is off.
+
+ Raises:
+ device_errors.CommandFailedError: If screen state cannot be found.
+```
+
+
+### DeviceUtils.SetScreen
+
+Turns screen on and off.
+```
+ Args:
+ on: bool to decide state to switch to. True = on False = off.
+```
+
+
+### GetAVDs
+
+Returns a list of Android Virtual Devices.
+```
+ Returns:
+ A list containing the configured AVDs.
+```
+
+
+### RestartServer
+
+Restarts the adb server.
+```
+ Raises:
+ CommandFailedError if we fail to kill or restart the server.
+```
+
+
diff --git a/catapult/devil/docs/markdown.md b/catapult/devil/docs/markdown.md
new file mode 100644
index 00000000..957dba7d
--- /dev/null
+++ b/catapult/devil/docs/markdown.md
@@ -0,0 +1,139 @@
+# [devil.utils.markdown](https://github.com/catapult-project/catapult/blob/master/devil/devil/utils/markdown.py)
+
+*This page was autogenerated by `devil/utils/markdown.py --module-link https://github.com/catapult-project/catapult/blob/master/devil/devil/utils/markdown.py`*
+
+## MarkdownHelpAction
+
+### MarkdownHelpAction.\_\_init\_\_
+
+### MarkdownHelpAction.\_\_call\_\_
+
+## MarkdownHelpFormatter
+
+A really bare-bones argparse help formatter that generates valid markdown.
+```
+ This will generate something like:
+
+ usage
+
+ # **section heading**:
+
+ ## **--argument-one**
+
+ \`\`\`
+ argument-one help text
+ \`\`\`
+
+```
+
+
+### MarkdownHelpFormatter.format\_help
+
+### MarkdownHelpFormatter.start\_section
+
+### md\_bold
+
+Returns markdown-formatted bold text.
+### md\_code
+
+Returns a markdown-formatted code block in the given language.
+### md\_escape
+
+Escapes \* and \_.
+### md\_heading
+
+Returns markdown-formatted heading.
+### md\_inline\_code
+
+Returns markdown-formatted inline code.
+### md\_italic
+
+Returns markdown-formatted italic text.
+### md\_link
+
+returns a markdown-formatted link.
+### add\_md\_help\_argument
+
+Adds --md-help to the given argparse.ArgumentParser.
+```
+ Running a script with --md-help will print the help text for that script
+ as valid markdown.
+
+ Args:
+ parser: The ArgumentParser to which --md-help should be added.
+```
+
+
+### load\_module\_from\_path
+
+Load a module given only the path name.
+```
+ Also loads package modules as necessary.
+
+ Args:
+ module_path: An absolute path to a python module.
+ Returns:
+ The module object for the given path.
+```
+
+
+### md\_module
+
+Write markdown documentation for a class.
+```
+ Documents public classes and functions.
+
+ Args:
+ class_obj: a types.TypeType object for the class that should be
+ documented.
+ Returns:
+ A list of markdown-formatted lines.
+```
+
+
+### md\_class
+
+Write markdown documentation for a class.
+```
+ Documents public methods. Does not currently document subclasses.
+
+ Args:
+ class_obj: a types.TypeType object for the class that should be
+ documented.
+ Returns:
+ A list of markdown-formatted lines.
+```
+
+
+### md\_docstring
+
+Write a markdown-formatted docstring.
+```
+ Returns:
+ A list of markdown-formatted lines.
+```
+
+
+### md\_function
+
+Write markdown documentation for a function.
+```
+ Args:
+ func_obj: a types.FunctionType object for the function that should be
+ documented.
+ Returns:
+ A list of markdown-formatted lines.
+```
+
+
+### main
+
+Write markdown documentation for the module at the provided path.
+```
+ Args:
+ raw_args: the raw command-line args. Usually sys.argv[1:].
+ Returns:
+ An integer exit code. 0 for success, non-zero for failure.
+```
+
+