aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil
diff options
context:
space:
mode:
authorIsabelle Taylor <taylori@google.com>2019-08-07 11:33:08 +0100
committerIsabelle Taylor <taylori@google.com>2019-08-07 14:55:45 +0000
commitc0de1d982ab4d1b375226ce8eaaba33d1d2c13b2 (patch)
treed00862cb1a1556c28c10469e6156b9d26ca1b51b /catapult/devil
parenta52622800007a447105f938c99a027cc8908a09b (diff)
downloadchromium-trace-c0de1d982ab4d1b375226ce8eaaba33d1d2c13b2.tar.gz
Update catapult to latest version (76a241669)
git log --oneline --no-merges 625dca847..76a241669 tracing systrace 76a241669 Update to use traceconv 1e6b12d6c merge SDP data into a trace b92719d94 [LCP] Support LargestContentfulPaint for Telemetry 1703718a7 Record traces as artifacts 6d47875c4 systrace: Update traceconv version 2b0f630c6 Clarify the error message for Trace View picture debugging 8a9aee74a Tracing: factor out serializing code 566ad9545 rendering: Add diagnostic data for janks. 24b441ff9 Implement Visually Complete at 2s Metric 0c9a952c8 Fixed symbolize of win traces with breakpad syms performed on linux f550dbf1f [catapult] Remove unit-info.json ef2cd5094 rendering: Include 'benchmark' for 'Rendering' and 'UI Rendering'. 0852b11fa interaction records: Use segments from user-model. 9398b3687 Fix media video playback metrics -- take 2 1750292dc Override traceUrl from telemetry by the one from command arg 0fa679da5 Support Cast trace symbolization 5343c596b Adding a min limit of frame segments to calulate frame_times 93cd912aa [metrics] Optimize V8 and Blink GC metrics c68abdbb8 Revert "Fix media video playback metrics." 3344e16e5 Fix media video playback metrics. f8aef9991 Update metrics for Blink GC e80772c3e Add a new total_rendering metric f239adeee Add layout shift score to loading metric. a948316f5 systrace: bump trace to text version 63ab0c82e [FCP++] Filter loading image and text candidate dad31e0cc systrace: update trace_to_text sha c9cf972dc rendering: Fix handling unsual traces. fd64d5d2d rendering: Fix running the metric on non-renderer stories. 0aa89afda Remove unused chrome_v8.json testdata file 578d0b904 rendering: Process presentation-timestamp if available. ff395373a Pass Blink image decode TRACE_EVENT timing to rendering_metrics. 4dd0e5d36 removed summary_options from metrics which are reporting only one value 7ec38a401 [ftrace importer] Improve dma-fence parser 79cc5111e [ftrace importer] Rename fence parser f164714b5 [tracing] Add a timeout to metrics calculations 876651e18 [tracing] Fix up OWNERS files. 4f15e5774 [tracing] Separate trace data writing and reading 2dd550aaf Respect enabled by default categories in "manually select settings" 5b26b374b [Telemetry/Metrics] Skip complexity of map_runner.py when only running a single trace. 8c4839fee Reland "[trace_data] Add AddTraceFileFor method" 2e2998717 Revert "[trace_data] Add AddTraceFileFor method" b00da16c3 [trace_data] Add AddTraceFileFor method 7b3e75637 [FCP++] Report FCP++ as Telemetry metric a9c4beb56 Add tracing:convert_chart_json gn target. a606a564b Avoid spread operator in findToplevelSchedulerTasks 7f8529c21 Display median and 90th% in multi-event-sub-view 072a6f14b Add a metric that tracks native code memory footprint. 7b04f5a1f [Tracing] Fix flow arrows for large traces 9c13a9911 Add grouping for Maps and DescriptorArrays. c8ea4478e Fixing values for timeToFirstViewportReady and aboveTheFoldLoadedToVisible 57a7c38dd rendering: Remove breakdowns for thread-time metrics. db7629d5b [PerSecond] Implement Per-second breakdown metrics 5193bcc10 Refactor duplicate getNetworkEventsInRange() 139441f05 [tracing] Add trace_data_builder.OpenTraceHandleFor 2d11abbb3 [systrace] Replace generate_random_filename_for_test with tempfile_ext bc2a48ba8 [tracing] Restrict writing unstructured trace data 1b12fa2c0 Protect add_reserved_diagnostics against empty HistogramSets. 71c6104a1 [tracing] Delete some dead code in trace_data 18f984114 [tracing] Prepare new API for TraceDataBuilder 45510bdf4 [tracing] Remove active_parts from trace data object 7b71bd28a Create chrome tracing alerts for missed frames 869dc82af Rendering Metrics: step 3 of removing redundant metrics 4f8e8775d tracing: Terminate work-queue if all tasks are done. 0039d0daf tracing: Do not start unnecessary threads. bf564e0be tracing: Handle error running vinn. 0513e38b9 Append ImageFrameGenerator::decodeToYUV to imageDecode tracing category 9d76caaa2 Add WebXR metric d297d566a Add testGotClockSyncMarkers() test 6ee619e4f Remove duplicate code from bad merge. 520d06f41 Deserialize a new HistogramSet JSON format. ca6432e20 HistogramSets: Validate loop index 0d1d8c12e Serialize a new HistogramSet JSON format. f6100576b Include category and name as part of flow ids 5c1c31e62 Add generateColors() for spa a4323c9b8 Split Histogram.statistics_scalars into statistics_names, GetStatisticScalar. fc67ab6f7 Serialize DiagnosticMap 285988459 Remove old VR aliases c4408e882 Deserialize DiagnosticMap. e4d7378b5 speed: Use gzip compression for trace data. 6fc8962f1 android_startup_metric: Add more diagnostic measurements e8ab7db4d Tracing: handle empty list of ranges 34d4c3e64 Short unit improvement direction suffixes 29b3f3626 Fix VR frameCycleDurationMetric 770b18b65 [tracing] Remove _TraceData.__getitem__ method 084703e4e Add Diagnostic.serialize(). 9f11b438f Add Diagnostic.deserialize() 04e1b2eeb Add stableId to memory dumps. b3d62293b Add HistogramSet OWNERS 0d71048a2 Revert "Rename add_reserved_diagnostics to prepare_upload, keep alias." 2e19f1c11 Add stableId to instant events. 6d35d61a1 Rename add_reserved_diagnostics to prepare_upload, keep alias. cb6a56815 [Telemetry] Change tb.StopTracing() to return just trace data 1a351b413 Chrome Tracing: Add jank_count for tracing 31798d055 Add HistogramSerializer 82959c838 Add HistogramDeserializer 700a8b6c1 Metrics: parse UMA histograms from traces 75a89f272 Updated brushing states for tracing UI. Events highlighted from Find Matches or from Analysis Views will be brightened instead of dimmed. Orginally, they were dimmed while all other events were also dimmed to grey, making it hard to spot the highlighted events (it seems this was introduced here: https://codereview.chromium.org/1429863004/patch/1/10007). c276258fd Reland add_reserved_diagnostics --max_size 610e624a4 [tracing] Make Python's _TraceData object private 64102c084 Revert "Output some debug info when getTraceCategories fails" cbea2ee3d Revert "Fix debug string for validateTraceCategories error." 38769c1f9 Remove spurious "o" added in crrev.com/c/1455628. Sorry! 130074321 Fix ftrace clock sync errors on CrOS 4e2870c4b Add a 'UI Rendering' option for recording. fb5956e43 Use start address provided by metadata for symbolizer 78b1c61ff [Telemetry] Remove inspector_importer b486e0449 Revert "add_reserved_diagnostics --max_size" fd97b5ad5 Fix debug string for validateTraceCategories error. adcf2fcf3 add_reserved_diagnostics --max_size da7318dd0 Output some debug info when getTraceCategories fails 828e54e90 Add python Histogram.Create(). 2f6aedbe4 adb_profile_chrome_startup: Enable Perfetto tracing back-end 514fe3e70 95% Confidence Intervals for mean statistic: cf13e722c Port legacy unit info to Python 25d49af2e Fix sum field in gtest_json_converter 348883443 Reland Remove TagMap. b98ec701f Revert "Ignore zeros in Breakdowns." c329a12f2 [ftrace importer] Add ion heap parser 78448d900 Plumb the trace buffer size for atrace 464917736 Fix handling of perfetto protobuf on mac 50be11941 Revert "Remove TagMap." df10be749 Fix gtest conversion multiplier 8eb35ff49 Adding timeToFirstViewportReady metric b89908927 Remove TagMap. 8dc09eb07 Truncate serialized floats in HistogramSet JSON. 6eeb1d2fc Remove support for legacy json formats from render_histograms_viewer. 222f2c0b2 Use the HTMLImports polyfill 9bfcc696e Relax an assertion in heap_profiler_unittest. 79517a0b0 Clean up a comment in generic_set.py. 3ad33e96e Ignore zeros in Breakdowns. 4ec170a7b Remove shortName from Histogram. 3511bed44 Remove RelatedHistograms from tbmv2. 550c3d05d Display link tuples in generic-set-span. 089fbe2fa Add a script to add a label to Histograms. 40737696f Assert that shared diagnostic types are valid. ebf0d23ee Add gtest perf test conversion script 16717a6dd Reland the safe parts of "Syntax updates for Polymer 2.0." d1cc7e545 Fix to show visualizations for multiple benchmarks in one results file 7c1d51b16 Histogram-based heap profiler f396a42a9 Add a way to visualize when raster tasks are performed. e90aa2a4b Fixes for Metrics Visualization 9310cd73f Replace RelatedHistogramMap with RelatedNameMap+Breakdown in EQT metric. 7d95d86a6 Replace RelatedHistogramMap with RelateNameMap+Breakdown in runtimeStatsMetric. 8964ef5af Changed average raster task times metric a382be45b Allow the caller to specify tracing categories for adb_profile_chrome_startup 296353761 Split metrics_visualization into two files 277cdb9bc Tab_Switching results calculation includes unwanted data b4581eb21 [ftrace importer] Add rss stat parser 8435aca79 [tracing] Check for both type and value presence d18d6c7c7 Remove precision check from the EQT metric calculation 4415dc697 Add more debug info for an EQT failure check 4f870714b Remove myself from Catapult OWNERS files ee85992c5 Fix atrace_agent on pre-M devices 3dc39538b Create method to add shared diagnostics to histograms not created through tbmv2 417678103 Added new pipeline metrics 82f5c3a6c Show hover text for transparent values and fixed hide Y Axis 27e047e04 Fix crashes on performance.measure name parsing. 5913160a7 Refactoring remaining diagnostics in histogram.py to diagnostics dir. b81a9c76c [soundwave] Run soundwave scripts using vpython 35d4c6c88 Fix merging single-bin Histograms with multi-bin Histograms. b0c06d4b4 rendering: Fix tasks_per_frame_ unit. 415cd78ad Telemetry: Fix a bug in sorting latency events c14a383e6 androidStartupMetric: skip FCP events when startup is missing a1878a947 Update computation of load bias to handle CrOS edge cases. d1eeca86a compare_samples: trivial fix in Main cdd16e094 Support uploading to dev_appserver.py 7ec848b2b Permit lack of value.name in convertChartJson. 44b1d78c3 androidStartupMetric: workarund for missing main entry point marker 03ce64d28 Telemetry: tasks_per_frame in TBMv2 468ff3e5a Ensure that the mutator utilization metric always makes progress. a8f088180 Telemetry: migrate thread_*_cpu_time_per_frame f92910970 Basic Python3 compatibility for catapult tracing a65a6b400 Fix summary options for the MMU metric histogram. Bug: 137786880 Test: manual run of ./systrace.py Merged-In: I2c1fbbefae7a751de17bf7e38da8a8fd2f4bd32f (cherry picked from commit eba0701e9764b55be4bb397184efc5c3f0e9c9ce) Change-Id: I2c1fbbefae7a751de17bf7e38da8a8fd2f4bd32f
Diffstat (limited to 'catapult/devil')
-rw-r--r--catapult/devil/BUILD.gn37
-rw-r--r--catapult/devil/README.md4
-rwxr-xr-xcatapult/devil/bin/run_py_tests6
-rw-r--r--catapult/devil/devil/android/apk_helper.py130
-rwxr-xr-xcatapult/devil/devil/android/apk_helper_test.py135
-rw-r--r--catapult/devil/devil/android/battery_utils.py38
-rwxr-xr-xcatapult/devil/devil/android/battery_utils_test.py51
-rw-r--r--catapult/devil/devil/android/cpu_temperature.py154
-rw-r--r--catapult/devil/devil/android/cpu_temperature_test.py132
-rw-r--r--catapult/devil/devil/android/crash_handler.py3
-rw-r--r--catapult/devil/devil/android/device_utils.py399
-rwxr-xr-xcatapult/devil/devil/android/device_utils_test.py881
-rw-r--r--catapult/devil/devil/android/fastboot_utils.py2
-rw-r--r--catapult/devil/devil/android/flag_changer.py22
-rw-r--r--catapult/devil/devil/android/logcat_monitor.py13
-rw-r--r--catapult/devil/devil/android/md5sum.py6
-rw-r--r--catapult/devil/devil/android/ndk/__init__.py6
-rw-r--r--catapult/devil/devil/android/ndk/abis.py16
-rw-r--r--catapult/devil/devil/android/perf/perf_control.py38
-rw-r--r--catapult/devil/devil/android/perf/surface_stats_collector.py118
-rw-r--r--catapult/devil/devil/android/perf/surface_stats_collector_test.py40
-rw-r--r--catapult/devil/devil/android/ports.py2
-rw-r--r--catapult/devil/devil/android/sdk/adb_wrapper.py89
-rwxr-xr-xcatapult/devil/devil/android/tools/device_recovery.py61
-rw-r--r--catapult/devil/devil/android/tools/script_common.py33
-rwxr-xr-xcatapult/devil/devil/android/tools/system_app.py67
-rwxr-xr-xcatapult/devil/devil/android/tools/system_app_devicetest.py2
-rw-r--r--catapult/devil/devil/android/tools/system_app_test.py66
-rwxr-xr-xcatapult/devil/devil/android/tools/webview_app.py205
-rw-r--r--catapult/devil/devil/devil_dependencies.json18
-rw-r--r--catapult/devil/devil/utils/cmd_helper.py10
-rw-r--r--catapult/devil/devil/utils/logging_common.py23
-rwxr-xr-xcatapult/devil/devil/utils/markdown.py4
-rw-r--r--catapult/devil/devil/utils/reraiser_thread.py21
-rwxr-xr-xcatapult/devil/devil/utils/reset_usb.py5
-rw-r--r--catapult/devil/devil/utils/run_tests_helper.py4
36 files changed, 2239 insertions, 602 deletions
diff --git a/catapult/devil/BUILD.gn b/catapult/devil/BUILD.gn
index 661a24fd..cf1255d6 100644
--- a/catapult/devil/BUILD.gn
+++ b/catapult/devil/BUILD.gn
@@ -2,20 +2,31 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-import("//build/symlink.gni")
-import("//testing/android/empty_apk/empty_apk.gni")
-
-empty_apk("empty_system_webview_apk") {
- package_name = "com.android.webview"
- apk_name = "EmptySystemWebView"
-}
-
group("devil") {
testonly = true
- deps = [
- ":empty_system_webview_apk",
- "//buildtools/third_party/libc++($host_toolchain)",
- "//tools/android/forwarder2",
- "//tools/android/md5sum",
+ deps = []
+ data_deps = [
+ "../third_party/gsutil",
+ ]
+ data = [
+ "devil/",
]
+
+ if (is_android) {
+ deps += [
+ ":empty_system_webview_apk",
+ "//buildtools/third_party/libc++($host_toolchain)",
+ "//tools/android/forwarder2",
+ "//tools/android/md5sum",
+ ]
+ }
+}
+
+if (is_android) {
+ import("//testing/android/empty_apk/empty_apk.gni")
+
+ empty_apk("empty_system_webview_apk") {
+ package_name = "com.android.webview"
+ apk_name = "EmptySystemWebView"
+ }
}
diff --git a/catapult/devil/README.md b/catapult/devil/README.md
index 852ac378..9953e6ae 100644
--- a/catapult/devil/README.md
+++ b/catapult/devil/README.md
@@ -6,8 +6,8 @@
😈
-devil is a library used by the Chromium developers to interact with Android
-devices. It currently supports SDK level 16 and above.
+devil (device interaction layer) is a library used by the Chromium developers to
+interact with Android devices. It currently supports SDK level 16 and above.
## Interfaces
diff --git a/catapult/devil/bin/run_py_tests b/catapult/devil/bin/run_py_tests
index 44ec61e8..a74fa838 100755
--- a/catapult/devil/bin/run_py_tests
+++ b/catapult/devil/bin/run_py_tests
@@ -16,6 +16,12 @@ from catapult_build import run_with_typ
def main():
+ # Tests mock out internal details of methods, and the ANDROID_SERIAL can
+ # change which internal methods are called. Since tests don't actually use
+ # devices, it should be fine to delete the variable.
+ if 'ANDROID_SERIAL' in os.environ:
+ del os.environ['ANDROID_SERIAL']
+
return run_with_typ.Run(top_level_dir=_DEVIL_PATH)
if __name__ == '__main__':
diff --git a/catapult/devil/devil/android/apk_helper.py b/catapult/devil/devil/android/apk_helper.py
index ab7649f8..abdf9071 100644
--- a/catapult/devil/devil/android/apk_helper.py
+++ b/catapult/devil/devil/android/apk_helper.py
@@ -5,10 +5,13 @@
"""Module containing utilities for apk packages."""
import re
+import xml.etree.ElementTree
import zipfile
from devil import base_error
+from devil.android.ndk import abis
from devil.android.sdk import aapt
+from devil.utils import cmd_helper
_MANIFEST_ATTRIBUTE_RE = re.compile(
@@ -45,9 +48,8 @@ def ToHelper(path_or_helper):
# matches the height of the stack). Each line parsed (either an attribute or an
# element) is added to the node at the top of the stack (after the stack has
# been popped/pushed due to indentation).
-def _ParseManifestFromApk(apk_path):
- aapt_output = aapt.Dump('xmltree', apk_path, 'AndroidManifest.xml')
-
+def _ParseManifestFromApk(apk):
+ aapt_output = aapt.Dump('xmltree', apk.path, 'AndroidManifest.xml')
parsed_manifest = {}
node_stack = [parsed_manifest]
indent = ' '
@@ -96,7 +98,8 @@ def _ParseManifestFromApk(apk_path):
manifest_key = m.group(1)
if manifest_key in node:
raise base_error.BaseError(
- "A single attribute should have one key and one value")
+ "A single attribute should have one key and one value: {}"
+ .format(line))
else:
node[manifest_key] = m.group(2) or m.group(3)
continue
@@ -104,6 +107,47 @@ def _ParseManifestFromApk(apk_path):
return parsed_manifest
+def _ParseManifestFromBundle(bundle):
+ cmd = [bundle.path, 'dump-manifest']
+ status, stdout, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
+ if status != 0:
+ raise Exception('Failed running {} with output\n{}\n{}'.format(
+ ' '.join(cmd), stdout, stderr))
+ return ParseManifestFromXml(stdout)
+
+
+def ParseManifestFromXml(xml_str):
+ """Parse an android bundle manifest.
+
+ As ParseManifestFromAapt, but uses the xml output from bundletool. Each
+ element is a dict, mapping attribute or children by name. Attributes map to
+ a dict (as they are unique), children map to a list of dicts (as there may
+ be multiple children with the same name).
+
+ Args:
+ xml_str (str) An xml string that is an android manifest.
+
+ Returns:
+ A dict holding the parsed manifest, as with ParseManifestFromAapt.
+ """
+ root = xml.etree.ElementTree.fromstring(xml_str)
+ return {root.tag: [_ParseManifestXMLNode(root)]}
+
+
+def _ParseManifestXMLNode(node):
+ out = {}
+ for name, value in node.attrib.items():
+ cleaned_name = name.replace(
+ '{http://schemas.android.com/apk/res/android}',
+ 'android:').replace(
+ '{http://schemas.android.com/tools}',
+ 'tools:')
+ out[cleaned_name] = value
+ for child in node:
+ out.setdefault(child.tag, []).append(_ParseManifestXMLNode(child))
+ return out
+
+
def _ParseNumericKey(obj, key, default=0):
val = obj.get(key)
if val is None:
@@ -152,6 +196,10 @@ class ApkHelper(object):
def path(self):
return self._apk_path
+ @property
+ def is_bundle(self):
+ return self._apk_path.endswith('_bundle')
+
def GetActivityName(self):
"""Returns the name of the first launcher Activity in the apk."""
manifest_info = self._GetManifest()
@@ -233,9 +281,73 @@ class ApkHelper(object):
except KeyError:
return []
+ def GetVersionCode(self):
+ """Returns the versionCode as an integer, or None if not available."""
+ manifest_info = self._GetManifest()
+ try:
+ version_code = manifest_info['manifest'][0]['android:versionCode']
+ return int(version_code, 16)
+ except KeyError:
+ return None
+
+ def GetVersionName(self):
+ """Returns the versionName as a string."""
+ manifest_info = self._GetManifest()
+ try:
+ version_name = manifest_info['manifest'][0]['android:versionName']
+ return version_name
+ except KeyError:
+ return ''
+
+ def GetMinSdkVersion(self):
+ """Returns the minSdkVersion as a string, or None if not available.
+
+ Note: this cannot always be cast to an integer."""
+ manifest_info = self._GetManifest()
+ try:
+ uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
+ min_sdk_version = uses_sdk['android:minSdkVersion']
+ try:
+ # The common case is for this to be an integer. Convert to decimal
+ # notation (rather than hexadecimal) for readability, but convert back
+ # to a string for type consistency with the general case.
+ return str(int(min_sdk_version, 16))
+ except ValueError:
+ # In general (ex. apps with minSdkVersion set to pre-release Android
+ # versions), minSdkVersion can be a string (usually, the OS codename
+ # letter). For simplicity, don't do any validation on the value.
+ return min_sdk_version
+ except KeyError:
+ return None
+
+ def GetTargetSdkVersion(self):
+ """Returns the targetSdkVersion as a string, or None if not available.
+
+ Note: this cannot always be cast to an integer."""
+ manifest_info = self._GetManifest()
+ try:
+ uses_sdk = manifest_info['manifest'][0]['uses-sdk'][0]
+ target_sdk_version = uses_sdk['android:targetSdkVersion']
+ try:
+ # The common case is for this to be an integer. Convert to decimal
+ # notation (rather than hexadecimal) for readability, but convert back
+ # to a string for type consistency with the general case.
+ return str(int(target_sdk_version, 16))
+ except ValueError:
+ # In general (ex. apps targeting pre-release Android versions),
+ # targetSdkVersion can be a string (usually, the OS codename letter).
+ # For simplicity, don't do any validation on the value.
+ return target_sdk_version
+ except KeyError:
+ return None
+
def _GetManifest(self):
if not self._manifest:
- self._manifest = _ParseManifestFromApk(self._apk_path)
+ app = ToHelper(self._apk_path)
+ if app.is_bundle:
+ self._manifest = _ParseManifestFromBundle(app)
+ else:
+ self._manifest = _ParseManifestFromApk(app)
return self._manifest
def _ResolveName(self, name):
@@ -257,10 +369,10 @@ class ApkHelper(object):
if len(path_tokens) >= 2 and path_tokens[0] == 'lib':
libs.add(path_tokens[1])
lib_to_abi = {
- 'armeabi-v7a': ['armeabi-v7a', 'arm64-v8a'],
- 'arm64-v8a': ['arm64-v8a'],
- 'x86': ['x86', 'x64'],
- 'x64': ['x64']
+ abis.ARM: [abis.ARM, abis.ARM_64],
+ abis.ARM_64: [abis.ARM_64],
+ abis.X86: [abis.X86, abis.X86_64],
+ abis.X86_64: [abis.X86_64]
}
try:
output = set()
diff --git a/catapult/devil/devil/android/apk_helper_test.py b/catapult/devil/devil/android/apk_helper_test.py
index 3be9d819..3258bb01 100755
--- a/catapult/devil/devil/android/apk_helper_test.py
+++ b/catapult/devil/devil/android/apk_helper_test.py
@@ -10,16 +10,23 @@ import unittest
from devil import base_error
from devil import devil_env
from devil.android import apk_helper
+from devil.android.ndk import abis
from devil.utils import mock_calls
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
+# pylint: disable=line-too-long
_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
E: manifest (line=1)
+ A: android:versionCode(0x0101021b)=(type 0x10)0x166de1ea
+ A: android:versionName(0x0101021c)="75.0.3763.0" (Raw: "75.0.3763.0")
A: package="org.chromium.abc" (Raw: "org.chromium.abc")
A: split="random_split" (Raw: "random_split")
+ E: uses-sdk (line=2)
+ A: android:minSdkVersion(0x0101020c)=(type 0x10)0x15
+ A: android:targetSdkVersion(0x01010270)=(type 0x10)0x1c
E: uses-permission (line=2)
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
E: uses-permission (line=3)
@@ -113,6 +120,14 @@ _SINGLE_J4_INSTRUMENTATION_MANIFEST_DUMP = """N: android=http://schemas.android.
A: junit4=(type 0x12)0xffffffff (Raw: "true")
"""
+_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP = """N: android=http://schemas.android.com/apk/res/android
+ E: manifest (line=1)
+ A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
+ E: uses-sdk (line=2)
+ A: android:minSdkVersion(0x0101020c)="Q" (Raw: "Q")
+ A: android:targetSdkVersion(0x01010270)="Q" (Raw: "Q")
+"""
+
_NO_NAMESPACE_MANIFEST_DUMP = """E: manifest (line=1)
A: package="org.chromium.xyz" (Raw: "org.chromium.xyz")
E: instrumentation (line=8)
@@ -120,6 +135,7 @@ _NO_NAMESPACE_MANIFEST_DUMP = """E: manifest (line=1)
A: http://schemas.android.com/apk/res/android:name(0x01010003)="org.chromium.RandomTestRunner" (Raw: "org.chromium.RandomTestRunner")
A: http://schemas.android.com/apk/res/android:targetPackage(0x01010021)="org.chromium.random_package" (Raw:"org.chromium.random_pacakge")
"""
+# pylint: enable=line-too-long
def _MockAaptDump(manifest_dump):
@@ -221,6 +237,36 @@ class ApkHelperTest(mock_calls.TestCase):
self.assertEquals([('name1', 'value1'), ('name2', 'value2')],
helper.GetAllMetadata())
+ def testGetVersionCode(self):
+ with _MockAaptDump(_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals(376300010, helper.GetVersionCode())
+
+ def testGetVersionName(self):
+ with _MockAaptDump(_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals('75.0.3763.0', helper.GetVersionName())
+
+ def testGetMinSdkVersion_integerValue(self):
+ with _MockAaptDump(_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals('21', helper.GetMinSdkVersion())
+
+ def testGetMinSdkVersion_stringValue(self):
+ with _MockAaptDump(_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals('Q', helper.GetMinSdkVersion())
+
+ def testGetTargetSdkVersion_integerValue(self):
+ with _MockAaptDump(_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals('28', helper.GetTargetSdkVersion())
+
+ def testGetTargetSdkVersion_stringValue(self):
+ with _MockAaptDump(_TARGETING_PRE_RELEASE_Q_MANIFEST_DUMP):
+ helper = apk_helper.ApkHelper('')
+ self.assertEquals('Q', helper.GetTargetSdkVersion())
+
def testGetSingleInstrumentationName_strippedNamespaces(self):
with _MockAaptDump(_NO_NAMESPACE_MANIFEST_DUMP):
helper = apk_helper.ApkHelper('')
@@ -229,8 +275,8 @@ class ApkHelperTest(mock_calls.TestCase):
def testGetArchitectures(self):
AbiPair = collections.namedtuple('AbiPair', ['abi32bit', 'abi64bit'])
- for abi_pair in [AbiPair('lib/armeabi-v7a', 'lib/arm64-v8a'),
- AbiPair('lib/x86', 'lib/x64')]:
+ for abi_pair in [AbiPair('lib/' + abis.ARM, 'lib/' + abis.ARM_64),
+ AbiPair('lib/' + abis.X86, 'lib/' + abis.X86_64)]:
with _MockListApkPaths([abi_pair.abi32bit]):
helper = apk_helper.ApkHelper('')
self.assertEquals(set([os.path.basename(abi_pair.abi32bit),
@@ -246,6 +292,91 @@ class ApkHelperTest(mock_calls.TestCase):
self.assertEquals(set([os.path.basename(abi_pair.abi64bit)]),
set(helper.GetAbis()))
+ def testParseXmlManifest(self):
+ self.assertEquals({
+ 'manifest': [
+ {'android:compileSdkVersion': '28',
+ 'android:versionCode': '2',
+ 'uses-sdk': [
+ {'android:minSdkVersion': '24',
+ 'android:targetSdkVersion': '28'}],
+ 'uses-permission': [
+ {'android:name':
+ 'android.permission.ACCESS_COARSE_LOCATION'},
+ {'android:name':
+ 'android.permission.ACCESS_NETWORK_STATE'}],
+ 'application': [
+ {'android:allowBackup': 'true',
+ 'android:extractNativeLibs': 'false',
+ 'android:fullBackupOnly': 'false',
+ 'meta-data': [
+ {'android:name': 'android.allow_multiple',
+ 'android:value': 'true'},
+ {'android:name': 'multiwindow',
+ 'android:value': 'true'}],
+ 'activity': [
+ {'android:configChanges': '0x00001fb3',
+ 'android:excludeFromRecents': 'true',
+ 'android:name': 'ChromeLauncherActivity',
+ 'intent-filter': [
+ {'action': [
+ {'android:name': 'dummy.action'}],
+ 'category': [
+ {'android:name': 'DAYDREAM'},
+ {'android:name': 'CARDBOARD'}]}]},
+ {'android:enabled': 'false',
+ 'android:name': 'MediaLauncherActivity',
+ 'intent-filter': [
+ {'tools:ignore': 'AppLinkUrlError',
+ 'action': [{'android:name': 'VIEW'}],
+ 'category': [{'android:name': 'DEFAULT'}],
+ 'data': [
+ {'android:mimeType': 'audio/*'},
+ {'android:mimeType': 'image/*'},
+ {'android:mimeType': 'video/*'},
+ {'android:scheme': 'file'},
+ {'android:scheme': 'content'}]}]}]}]}]},
+ apk_helper.ParseManifestFromXml("""
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:compileSdkVersion="28" android:versionCode="2">
+ <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="28"/>
+ <uses-permission
+ android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <application android:allowBackup="true"
+ android:extractNativeLibs="false"
+ android:fullBackupOnly="false">
+ <meta-data android:name="android.allow_multiple"
+ android:value="true"/>
+ <meta-data android:name="multiwindow"
+ android:value="true"/>
+ <activity android:configChanges="0x00001fb3"
+ android:excludeFromRecents="true"
+ android:name="ChromeLauncherActivity">
+ <intent-filter>
+ <action android:name="dummy.action"/>
+ <category android:name="DAYDREAM"/>
+ <category android:name="CARDBOARD"/>
+ </intent-filter>
+ </activity>
+ <activity android:enabled="false"
+ android:name="MediaLauncherActivity">
+ <intent-filter tools:ignore="AppLinkUrlError">
+ <action android:name="VIEW"/>
+
+ <category android:name="DEFAULT"/>
+
+ <data android:mimeType="audio/*"/>
+ <data android:mimeType="image/*"/>
+ <data android:mimeType="video/*"/>
+ <data android:scheme="file"/>
+ <data android:scheme="content"/>
+ </intent-filter>
+ </activity>
+ </application>
+ </manifest>"""))
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/battery_utils.py b/catapult/devil/devil/android/battery_utils.py
index 9c83b5b0..c41c19a2 100644
--- a/catapult/devil/devil/android/battery_utils.py
+++ b/catapult/devil/devil/android/battery_utils.py
@@ -241,44 +241,6 @@ class BatteryUtils(object):
'Unable to find fuel gauge.')
@decorators.WithTimeoutAndRetriesFromInstance()
- def GetNetworkData(self, package, timeout=None, retries=None):
- """Get network data for specific package.
-
- Args:
- package: package name you want network data for.
- timeout: timeout in seconds
- retries: number of retries
-
- Returns:
- Tuple of (sent_data, recieved_data)
- None if no network data found
- """
- # If device_utils clears cache, cache['uids'] doesn't exist
- if 'uids' not in self._cache:
- self._cache['uids'] = {}
- if package not in self._cache['uids']:
- self.GetPowerData()
- if package not in self._cache['uids']:
- logger.warning('No UID found for %s. Can\'t get network data.',
- package)
- return None
-
- network_data_path = '/proc/uid_stat/%s/' % self._cache['uids'][package]
- try:
- send_data = int(self._device.ReadFile(network_data_path + 'tcp_snd'))
- # If ReadFile throws exception, it means no network data usage file for
- # package has been recorded. Return 0 sent and 0 received.
- except device_errors.AdbShellCommandFailedError:
- logger.warning('No sent data found for package %s', package)
- send_data = 0
- try:
- recv_data = int(self._device.ReadFile(network_data_path + 'tcp_rcv'))
- except device_errors.AdbShellCommandFailedError:
- logger.warning('No received data found for package %s', package)
- recv_data = 0
- return (send_data, recv_data)
-
- @decorators.WithTimeoutAndRetriesFromInstance()
def GetPowerData(self, timeout=None, retries=None):
"""Get power data for device.
diff --git a/catapult/devil/devil/android/battery_utils_test.py b/catapult/devil/devil/android/battery_utils_test.py
index feccf79e..07c74967 100755
--- a/catapult/devil/devil/android/battery_utils_test.py
+++ b/catapult/devil/devil/android/battery_utils_test.py
@@ -401,57 +401,6 @@ class BatteryUtilsGetChargingTest(BatteryUtilsTest):
self.assertFalse(self.battery.GetCharging())
-class BatteryUtilsGetNetworkDataTest(BatteryUtilsTest):
-
- def testGetNetworkData_noDataUsage(self):
- with self.assertCalls(
- (self.call.device.RunShellCommand(
- ['dumpsys', 'batterystats', '-c'],
- check_return=True, large_output=True),
- _DUMPSYS_OUTPUT),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'),
- self.ShellError()),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'),
- self.ShellError())):
- self.assertEquals(self.battery.GetNetworkData('test_package1'), (0, 0))
-
- def testGetNetworkData_badPackage(self):
- with self.assertCall(
- self.call.device.RunShellCommand(
- ['dumpsys', 'batterystats', '-c'],
- check_return=True, large_output=True),
- _DUMPSYS_OUTPUT):
- self.assertEqual(self.battery.GetNetworkData('asdf'), None)
-
- def testGetNetworkData_packageNotCached(self):
- with self.assertCalls(
- (self.call.device.RunShellCommand(
- ['dumpsys', 'batterystats', '-c'],
- check_return=True, large_output=True),
- _DUMPSYS_OUTPUT),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'), 1),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'), 2)):
- self.assertEqual(self.battery.GetNetworkData('test_package1'), (1, 2))
-
- def testGetNetworkData_packageCached(self):
- self.battery._cache['uids'] = {'test_package1': '1000'}
- with self.assertCalls(
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'), 1),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'), 2)):
- self.assertEqual(self.battery.GetNetworkData('test_package1'), (1, 2))
-
- def testGetNetworkData_clearedCache(self):
- with self.assertCalls(
- (self.call.device.RunShellCommand(
- ['dumpsys', 'batterystats', '-c'],
- check_return=True, large_output=True),
- _DUMPSYS_OUTPUT),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_snd'), 1),
- (self.call.device.ReadFile('/proc/uid_stat/1000/tcp_rcv'), 2)):
- self.battery._cache.clear()
- self.assertEqual(self.battery.GetNetworkData('test_package1'), (1, 2))
-
-
class BatteryUtilsLetBatteryCoolToTemperatureTest(BatteryUtilsTest):
@mock.patch('time.sleep', mock.Mock())
diff --git a/catapult/devil/devil/android/cpu_temperature.py b/catapult/devil/devil/android/cpu_temperature.py
new file mode 100644
index 00000000..58ce87a0
--- /dev/null
+++ b/catapult/devil/devil/android/cpu_temperature.py
@@ -0,0 +1,154 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Provides device interactions for CPU temperature monitoring."""
+# pylint: disable=unused-argument
+
+import logging
+
+from devil.android import device_utils
+from devil.android.perf import perf_control
+from devil.utils import timeout_retry
+
+logger = logging.getLogger(__name__)
+
+# NB: when adding devices to this structure, be aware of the impact it may
+# have on the chromium.perf waterfall, as it may increase testing time.
+# Please contact a person responsible for the waterfall to see if the
+# device you're adding is currently being tested.
+_DEVICE_THERMAL_INFORMATION = {
+ # Pixel 3
+ 'blueline': {
+ 'cpu_temps': {
+ # See /sys/class/thermal/thermal_zone<number>/type for description
+ # Types:
+ # cpu0: cpu0-silver-step
+ # cpu1: cpu1-silver-step
+ # cpu2: cpu2-silver-step
+ # cpu3: cpu3-silver-step
+ # cpu4: cpu0-gold-step
+ # cpu5: cpu1-gold-step
+ # cpu6: cpu2-gold-step
+ # cpu7: cpu3-gold-step
+ 'cpu0': '/sys/class/thermal/thermal_zone11/temp',
+ 'cpu1': '/sys/class/thermal/thermal_zone12/temp',
+ 'cpu2': '/sys/class/thermal/thermal_zone13/temp',
+ 'cpu3': '/sys/class/thermal/thermal_zone14/temp',
+ 'cpu4': '/sys/class/thermal/thermal_zone15/temp',
+ 'cpu5': '/sys/class/thermal/thermal_zone16/temp',
+ 'cpu6': '/sys/class/thermal/thermal_zone17/temp',
+ 'cpu7': '/sys/class/thermal/thermal_zone18/temp'
+ },
+ # Different device sensors use different multipliers
+ # e.g. Pixel 3 35 degrees c is 35000
+ 'temp_multiplier': 1000
+ },
+ # Pixel
+ 'sailfish': {
+ 'cpu_temps': {
+ # The following thermal zones tend to produce the most accurate
+ # readings
+ # Types:
+ # cpu0: tsens_tz_sensor0
+ # cpu1: tsens_tz_sensor1
+ # cpu2: tsens_tz_sensor2
+ # cpu3: tsens_tz_sensor3
+ 'cpu0': '/sys/class/thermal/thermal_zone1/temp',
+ 'cpu1': '/sys/class/thermal/thermal_zone2/temp',
+ 'cpu2': '/sys/class/thermal/thermal_zone3/temp',
+ 'cpu3': '/sys/class/thermal/thermal_zone4/temp'
+ },
+ 'temp_multiplier': 10
+ }
+}
+
+
+class CpuTemperature(object):
+
+ def __init__(self, device):
+ """CpuTemperature constructor.
+
+ Args:
+ device: A DeviceUtils instance.
+ Raises:
+ TypeError: If it is not passed a DeviceUtils instance.
+ """
+ if not isinstance(device, device_utils.DeviceUtils):
+ raise TypeError('Must be initialized with DeviceUtils object.')
+ self._device = device
+ self._perf_control = perf_control.PerfControl(self._device)
+ self._device_info = None
+
+ def InitThermalDeviceInformation(self):
+ """Init the current devices thermal information.
+ """
+ self._device_info = _DEVICE_THERMAL_INFORMATION.get(
+ self._device.build_product)
+
+ def IsSupported(self):
+ """Check if the current device is supported.
+
+ Returns:
+ True if the device is in _DEVICE_THERMAL_INFORMATION and the temp
+ files exist. False otherwise.
+ """
+ # Init device info if it hasnt been manually initialised already
+ if self._device_info is None:
+ self.InitThermalDeviceInformation()
+
+ if self._device_info is not None:
+ return all(
+ self._device.FileExists(f)
+ for f in self._device_info['cpu_temps'].values())
+ return False
+
+ def LetCpuCoolToTemperature(self, target_temp, wait_period=30):
+ """Lets device sit to give CPU time to cool down.
+
+ Implements a similar mechanism to
+ battery_utils.LetBatteryCoolToTemperature
+
+ Args:
+ temp: A float containing the maximum temperature to allow
+ in degrees c.
+ wait_period: An integer indicating time in seconds to wait
+ between checking.
+ """
+ target_temp = int(target_temp * self._device_info['temp_multiplier'])
+
+ def cool_cpu():
+ # Get the temperatures
+ cpu_temp_paths = self._device_info['cpu_temps']
+ temps = []
+ for temp_path in cpu_temp_paths.values():
+ temp_return = self._device.ReadFile(temp_path)
+ # Output is an array of strings, only need the first line.
+ temps.append(int(temp_return))
+
+ if not temps:
+ logger.warning('Unable to read temperature files provided.')
+ return True
+
+ logger.info('Current CPU temperatures: %s', str(temps)[1:-1])
+
+ return all(t <= target_temp for t in temps)
+
+ logger.info('Waiting for the CPU to cool down to %s',
+ target_temp / self._device_info['temp_multiplier'])
+
+ # Set the governor to powersave to aid the cooling down of the CPU
+ self._perf_control.SetScalingGovernor('powersave')
+
+ # Retry 3 times, each time waiting 30 seconds.
+ # This negates most (if not all) of the noise in recorded results without
+ # taking too long
+ timeout_retry.WaitFor(cool_cpu, wait_period=wait_period, max_tries=3)
+
+ # Set the performance mode
+ self._perf_control.SetHighPerfMode()
+
+ def GetDeviceForTesting(self):
+ return self._device
+
+ def GetDeviceInfoForTesting(self):
+ return self._device_info
diff --git a/catapult/devil/devil/android/cpu_temperature_test.py b/catapult/devil/devil/android/cpu_temperature_test.py
new file mode 100644
index 00000000..f0f99de0
--- /dev/null
+++ b/catapult/devil/devil/android/cpu_temperature_test.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""
+Unit tests for the contents of cpu_temperature.py
+"""
+
+# pylint: disable=unused-argument
+
+import logging
+import unittest
+
+from devil import devil_env
+from devil.android import cpu_temperature
+from devil.android import device_utils
+from devil.utils import mock_calls
+from devil.android.sdk import adb_wrapper
+
+with devil_env.SysPath(devil_env.PYMOCK_PATH):
+ import mock # pylint: disable=import-error
+
+
+class CpuTemperatureTest(mock_calls.TestCase):
+
+ @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+ def setUp(self):
+ # Mock the device
+ self.mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+ self.mock_device.build_product = 'blueline'
+ self.mock_device.adb = mock.Mock(spec=adb_wrapper.AdbWrapper)
+ self.mock_device.FileExists.return_value = True
+
+ self.cpu_temp = cpu_temperature.CpuTemperature(self.mock_device)
+ self.cpu_temp.InitThermalDeviceInformation()
+
+
+class CpuTemperatureInitTest(unittest.TestCase):
+
+ @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+ def testInitWithDeviceUtil(self):
+ d = mock.Mock(spec=device_utils.DeviceUtils)
+ d.build_product = 'blueline'
+ c = cpu_temperature.CpuTemperature(d)
+ self.assertEqual(d, c.GetDeviceForTesting())
+
+ def testInitWithMissing_fails(self):
+ with self.assertRaises(TypeError):
+ cpu_temperature.CpuTemperature(None)
+ with self.assertRaises(TypeError):
+ cpu_temperature.CpuTemperature('')
+
+
+class CpuTemperatureGetThermalDeviceInformationTest(CpuTemperatureTest):
+
+ @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+ def testGetThermalDeviceInformation_noneWhenIncorrectLabel(self):
+ invalid_device = mock.Mock(spec=device_utils.DeviceUtils)
+ invalid_device.build_product = 'invalid_name'
+ c = cpu_temperature.CpuTemperature(invalid_device)
+ c.InitThermalDeviceInformation()
+ self.assertEqual(c.GetDeviceInfoForTesting(), None)
+
+ def testGetThermalDeviceInformation_getsCorrectInformation(self):
+ correct_information = {
+ 'cpu0': '/sys/class/thermal/thermal_zone11/temp',
+ 'cpu1': '/sys/class/thermal/thermal_zone12/temp',
+ 'cpu2': '/sys/class/thermal/thermal_zone13/temp',
+ 'cpu3': '/sys/class/thermal/thermal_zone14/temp',
+ 'cpu4': '/sys/class/thermal/thermal_zone15/temp',
+ 'cpu5': '/sys/class/thermal/thermal_zone16/temp',
+ 'cpu6': '/sys/class/thermal/thermal_zone17/temp',
+ 'cpu7': '/sys/class/thermal/thermal_zone18/temp'
+ }
+ self.assertEqual(
+ cmp(correct_information,
+ self.cpu_temp.GetDeviceInfoForTesting().get('cpu_temps')), 0)
+
+
+class CpuTemperatureIsSupportedTest(CpuTemperatureTest):
+
+ @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+ def testIsSupported_returnsTrue(self):
+ d = mock.Mock(spec=device_utils.DeviceUtils)
+ d.build_product = 'blueline'
+ d.FileExists.return_value = True
+ c = cpu_temperature.CpuTemperature(d)
+ self.assertTrue(c.IsSupported())
+
+ @mock.patch('devil.android.perf.perf_control.PerfControl', mock.Mock())
+ def testIsSupported_returnsFalse(self):
+ d = mock.Mock(spec=device_utils.DeviceUtils)
+ d.build_product = 'blueline'
+ d.FileExists.return_value = False
+ c = cpu_temperature.CpuTemperature(d)
+ self.assertFalse(c.IsSupported())
+
+
+class CpuTemperatureLetCpuCoolToTemperatureTest(CpuTemperatureTest):
+ # Return values for the mock side effect
+ cooling_down0 = ([45000 for _ in range(8)] + [43000 for _ in range(8)] +
+ [41000 for _ in range(8)])
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testLetBatteryCoolToTemperature_coolWithin24Calls(self):
+ self.mock_device.ReadFile = mock.Mock(side_effect=self.cooling_down0)
+ self.cpu_temp.LetCpuCoolToTemperature(42)
+ self.mock_device.ReadFile.assert_called()
+ self.assertEquals(self.mock_device.ReadFile.call_count, 24)
+
+ cooling_down1 = [45000 for _ in range(8)] + [41000 for _ in range(16)]
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testLetBatteryCoolToTemperature_coolWithin16Calls(self):
+ self.mock_device.ReadFile = mock.Mock(side_effect=self.cooling_down1)
+ self.cpu_temp.LetCpuCoolToTemperature(42)
+ self.mock_device.ReadFile.assert_called()
+ self.assertEquals(self.mock_device.ReadFile.call_count, 16)
+
+ constant_temp = [45000 for _ in range(40)]
+
+ @mock.patch('time.sleep', mock.Mock())
+ def testLetBatteryCoolToTemperature_timeoutAfterThree(self):
+ self.mock_device.ReadFile = mock.Mock(side_effect=self.constant_temp)
+ self.cpu_temp.LetCpuCoolToTemperature(42)
+ self.mock_device.ReadFile.assert_called()
+ self.assertEquals(self.mock_device.ReadFile.call_count, 24)
+
+
+if __name__ == '__main__':
+ logging.getLogger().setLevel(logging.DEBUG)
+ unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/crash_handler.py b/catapult/devil/devil/android/crash_handler.py
index 7cfabcfb..028e787d 100644
--- a/catapult/devil/devil/android/crash_handler.py
+++ b/catapult/devil/devil/android/crash_handler.py
@@ -37,6 +37,9 @@ def RetryOnSystemCrash(f, device, retries=3):
raise
try:
logger.warning('Device is unreachable. Waiting for recovery...')
+ # Treat the device being unreachable as an unexpected reboot and clear
+ # any cached state.
+ device.ClearCache()
device.WaitUntilFullyBooted()
except base_error.BaseError:
logger.exception('Device never recovered. X(')
diff --git a/catapult/devil/devil/android/device_utils.py b/catapult/devil/devil/android/device_utils.py
index 575865bd..6182a527 100644
--- a/catapult/devil/devil/android/device_utils.py
+++ b/catapult/devil/devil/android/device_utils.py
@@ -10,6 +10,7 @@ Eventually, this will be based on adb_wrapper.
import calendar
import collections
+import contextlib
import fnmatch
import json
import logging
@@ -21,6 +22,7 @@ import random
import re
import shutil
import stat
+import sys
import tempfile
import time
import threading
@@ -50,6 +52,12 @@ from devil.utils import zip_utils
from py_utils import tempfile_ext
+try:
+ from devil.utils import reset_usb
+except ImportError:
+ # Fail silently if we can't import reset_usb. We're likely on windows.
+ reset_usb = None
+
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
@@ -212,7 +220,12 @@ _SPECIAL_ROOT_DEVICE_LIST = [
'taimen', # Pixel 2 XL
'vega', # Lenovo Mirage Solo
'walleye', # Pixel 2
+ 'crosshatch', # Pixel 3 XL
+ 'blueline', # Pixel 3
]
+_SPECIAL_ROOT_DEVICE_LIST += ['aosp_%s' % _d for _d in
+ _SPECIAL_ROOT_DEVICE_LIST]
+
_IMEI_RE = re.compile(r' Device ID = (.+)$')
# The following regex is used to match result parcels like:
"""
@@ -226,6 +239,25 @@ _PARCEL_RESULT_RE = re.compile(
_EBUSY_RE = re.compile(
r'mkdir failed for ([^,]*), Device or resource busy')
+# http://bit.ly/2WLZhUF added a timeout to adb wait-for-device. We sometimes
+# want to wait longer than the implicit call within adb root allows.
+_WAIT_FOR_DEVICE_TIMEOUT_STR = 'timeout expired while waiting for device'
+
+_WEBVIEW_SYSUPDATE_CURRENT_PKG_RE = re.compile(
+ r'Current WebView package.*:.*\(([a-z.]*),')
+_WEBVIEW_SYSUPDATE_NULL_PKG_RE = re.compile(
+ r'Current WebView package is null')
+_WEBVIEW_SYSUPDATE_FALLBACK_LOGIC_RE = re.compile(
+ r'Fallback logic enabled: (true|false)')
+_WEBVIEW_SYSUPDATE_PACKAGE_INSTALLED_RE = re.compile(
+ r'(?:Valid|Invalid) package\s+(\S+)\s+\(.*\),?\s+(.*)$')
+_WEBVIEW_SYSUPDATE_PACKAGE_NOT_INSTALLED_RE = re.compile(
+ r'(\S+)\s+(is NOT installed\.)')
+_WEBVIEW_SYSUPDATE_MIN_VERSION_CODE = re.compile(
+ r'Minimum WebView version code: (\d+)')
+
+_GOOGLE_FEATURES_RE = re.compile(r'^\s*com\.google\.')
+
PS_COLUMNS = ('name', 'pid', 'ppid')
ProcessInfo = collections.namedtuple('ProcessInfo', PS_COLUMNS)
@@ -369,7 +401,7 @@ class DeviceUtils(object):
assert hasattr(self, decorators.DEFAULT_TIMEOUT_ATTR)
assert hasattr(self, decorators.DEFAULT_RETRIES_ATTR)
- self._ClearCache()
+ self.ClearCache()
@property
def serial(self):
@@ -453,6 +485,10 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
try:
+ if self.build_type == 'eng':
+ # 'eng' builds have root enabled by default and the adb session cannot
+ # be unrooted.
+ return True
if self.product_name in _SPECIAL_ROOT_DEVICE_LIST:
return self.GetProp('service.adb.root') == '1'
self.RunShellCommand(['ls', '/root'], check_return=True)
@@ -516,17 +552,22 @@ class DeviceUtils(object):
try:
self.adb.Root()
- except device_errors.AdbCommandFailedError:
+ except device_errors.AdbCommandFailedError as e:
if self.IsUserBuild():
raise device_errors.CommandFailedError(
'Unable to root device with user build.', str(self))
+ elif e.output and _WAIT_FOR_DEVICE_TIMEOUT_STR in e.output:
+ # adb 1.0.41 added a call to wait-for-device *inside* root
+ # with a timeout that can be too short in some cases.
+ # If we hit that timeout, ignore it & do our own wait below.
+ pass
else:
raise # Failed probably due to some other reason.
def device_online_with_root():
try:
self.adb.WaitForDevice()
- return self.GetProp('service.adb.root', cache=False) == '1'
+ return self.HasRoot()
except (device_errors.AdbCommandFailedError,
device_errors.DeviceUnreachableError):
return False
@@ -841,29 +882,35 @@ class DeviceUtils(object):
return not self.IsOnline()
self.adb.Reboot()
- self._ClearCache()
+ self.ClearCache()
timeout_retry.WaitFor(device_offline, wait_period=1)
if block:
self.WaitUntilFullyBooted(wifi=wifi)
- INSTALL_DEFAULT_TIMEOUT = 4 * _DEFAULT_TIMEOUT
+ INSTALL_DEFAULT_TIMEOUT = 8 * _DEFAULT_TIMEOUT
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=INSTALL_DEFAULT_TIMEOUT)
def Install(self, apk, allow_downgrade=False, reinstall=False,
- permissions=None, timeout=None, retries=None):
- """Install an APK.
+ permissions=None, timeout=None, retries=None, modules=None):
+ """Install an APK or app bundle.
- Noop if an identical APK is already installed.
+ Noop if an identical APK is already installed. If installing a bundle, the
+ bundletools helper script (bin/*_bundle) should be used rather than the .aab
+ file.
Args:
- apk: An ApkHelper instance or string containing the path to the APK.
+ apk: An ApkHelper instance or string containing the path to the APK or
+ bundle.
allow_downgrade: A boolean indicating if we should allow downgrades.
reinstall: A boolean indicating if we should keep any existing app data.
+ Ignored if |apk| is a bundle.
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
+ modules: An iterable containing specific bundle modules to install.
+ Error if set and |apk| points to an APK instead of a bundle.
Raises:
CommandFailedError if the installation fails.
@@ -871,7 +918,8 @@ class DeviceUtils(object):
DeviceUnreachableError on missing device.
"""
self._InstallInternal(apk, None, allow_downgrade=allow_downgrade,
- reinstall=reinstall, permissions=permissions)
+ reinstall=reinstall, permissions=permissions,
+ modules=modules)
@decorators.WithTimeoutAndRetriesFromInstance(
min_default_timeout=INSTALL_DEFAULT_TIMEOUT)
@@ -907,12 +955,29 @@ class DeviceUtils(object):
def _InstallInternal(self, base_apk, split_apks, allow_downgrade=False,
reinstall=False, allow_cached_props=False,
- permissions=None):
+ permissions=None, modules=None):
+ base_apk = apk_helper.ToHelper(base_apk)
+ if base_apk.is_bundle:
+ if split_apks:
+ raise device_errors.CommandFailedError(
+ 'Attempted to install a bundle {} while specifying split apks'
+ .format(base_apk))
+ if allow_downgrade:
+ logging.warning('Installation of a bundle requested with '
+ 'allow_downgrade=False. This is not possible with '
+ 'bundletools, no downgrading is possible. This '
+ 'flag will be ignored and installation will proceed.')
+ # |allow_cached_props| is unused and ignored for bundles.
+ self._InstallBundleInternal(base_apk, permissions, modules)
+ return
+
+ if modules:
+ raise device_errors.CommandFailedError(
+ 'Attempted to specify modules to install when providing an APK')
+
if split_apks:
self._CheckSdkLevel(version_codes.LOLLIPOP)
- base_apk = apk_helper.ToHelper(base_apk)
-
all_apks = [base_apk.path]
if split_apks:
all_apks += split_select.SelectSplits(
@@ -949,9 +1014,11 @@ class DeviceUtils(object):
logger.warning('Error calculating md5: %s', e)
apks_to_install, host_checksums = all_apks, None
if apks_to_install and not reinstall:
- self.Uninstall(package_name)
apks_to_install = all_apks
+ if device_apk_paths and apks_to_install and not reinstall:
+ self.Uninstall(package_name)
+
if apks_to_install:
# Assume that we won't know the resulting device state.
self._cache['package_apk_paths'].pop(package_name, 0)
@@ -977,6 +1044,20 @@ class DeviceUtils(object):
if host_checksums is not None:
self._cache['package_apk_checksums'][package_name] = host_checksums
+ def _InstallBundleInternal(self, bundle, permissions, modules):
+ cmd = [bundle.path, 'install', '--device', self.serial]
+ if modules:
+ for m in modules:
+ cmd.extend(['-m', m])
+ status = cmd_helper.RunCmd(cmd)
+ if status != 0:
+ raise device_errors.CommandFailedError('Cound not install {}'.format(
+ bundle.path))
+ if (permissions is None
+ and self.build_version_sdk >= version_codes.MARSHMALLOW):
+ permissions = bundle.GetPermissions()
+ self.GrantPermissions(bundle.GetPackageName(), permissions)
+
@decorators.WithTimeoutAndRetriesFromInstance()
def Uninstall(self, package_name, keep_data=False, timeout=None,
retries=None):
@@ -998,15 +1079,11 @@ class DeviceUtils(object):
installed = self._GetApplicationPathsInternal(package_name)
if not installed:
return
- try:
- self.adb.Uninstall(package_name, keep_data)
- self._cache['package_apk_paths'][package_name] = []
- self._cache['package_apk_checksums'][package_name] = set()
- except:
- # Clear cache since we can't be sure of the state.
- self._cache['package_apk_paths'].pop(package_name, 0)
- self._cache['package_apk_checksums'].pop(package_name, 0)
- raise
+ # cached package paths are indeterminate due to system apps taking over
+ # user apps after uninstall, so clear it
+ self._cache['package_apk_paths'].pop(package_name, 0)
+ self._cache['package_apk_checksums'].pop(package_name, 0)
+ self.adb.Uninstall(package_name, keep_data)
def _CheckSdkLevel(self, required_sdk_level):
"""Raises an exception if the device does not have the required SDK level.
@@ -1020,8 +1097,8 @@ class DeviceUtils(object):
@decorators.WithTimeoutAndRetriesFromInstance()
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,
- ensure_logs_on_timeout=False, timeout=None, retries=None):
+ 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
@@ -1064,10 +1141,6 @@ class DeviceUtils(object):
this large output will be truncated.
raw_output: Whether to only return the raw output
(no splitting into lines).
- ensure_logs_on_timeout: If True, will use a slightly smaller timeout for
- the internal adb command, which allows to retrive logs on timeout.
- Note that that logs are not guaranteed to be produced with this option
- as adb command may still hang and fail to respect the reduced timeout.
timeout: timeout in seconds
retries: number of retries
@@ -1091,7 +1164,7 @@ class DeviceUtils(object):
return '%s=%s' % (key, cmd_helper.DoubleQuote(value))
def run(cmd):
- return self.adb.Shell(cmd, ensure_logs_on_timeout=ensure_logs_on_timeout)
+ return self.adb.Shell(cmd)
def handle_check_return(cmd):
try:
@@ -1115,11 +1188,16 @@ class DeviceUtils(object):
def handle_large_output(cmd, large_output_mode):
if large_output_mode:
with device_temp_file.DeviceTempFile(self.adb) as large_output_file:
- cmd = '( %s )>%s 2>&1' % (cmd, large_output_file.name)
+ large_output_cmd = '( %s )>%s 2>&1' % (cmd, large_output_file.name)
logger.debug('Large output mode enabled. Will write output to '
'device and read results from file.')
- handle_large_command(cmd)
- return self.ReadFile(large_output_file.name, force_pull=True)
+ try:
+ handle_large_command(large_output_cmd)
+ return self.ReadFile(large_output_file.name, force_pull=True)
+ except device_errors.AdbShellCommandFailedError as exc:
+ output = self.ReadFile(large_output_file.name, force_pull=True)
+ raise device_errors.AdbShellCommandFailedError(
+ cmd, output, exc.status, exc.device_serial)
else:
try:
return handle_large_command(cmd)
@@ -1869,8 +1947,29 @@ class DeviceUtils(object):
device_path if not rename else [_RenamePath(p) for p in device_path])
self.RunShellCommand(args, as_root=as_root, check_return=True)
+ @contextlib.contextmanager
+ def _CopyToReadableLocation(self, device_path):
+ """Context manager to copy a file to a globally readable temp file.
+
+ This uses root permission to copy a file to a globally readable named
+ temporary file. The temp file is removed when this contextmanager is closed.
+
+ Args:
+ device_path: A string containing the absolute path of the file (on the
+ device) to copy.
+ Yields:
+ The globally readable file object.
+ """
+ with device_temp_file.DeviceTempFile(self.adb) as device_temp:
+ 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, shell=True, as_root=True, check_return=True)
+ yield device_temp
+
@decorators.WithTimeoutAndRetriesFromInstance()
- def PullFile(self, device_path, host_path, timeout=None, retries=None):
+ def PullFile(self, device_path, host_path, as_root=False, timeout=None,
+ retries=None):
"""Pull a file from the device.
Args:
@@ -1878,6 +1977,7 @@ class DeviceUtils(object):
from the device.
host_path: A string containing the absolute path of the destination on
the host.
+ as_root: Whether root permissions should be used to pull the file.
timeout: timeout in seconds
retries: number of retries
@@ -1889,7 +1989,14 @@ class DeviceUtils(object):
dirname = os.path.dirname(host_path)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
- self.adb.Pull(device_path, host_path)
+ if as_root and self.NeedsSU():
+ if not self.PathExists(device_path, as_root=True):
+ raise device_errors.CommandFailedError(
+ '%r: No such file or directory' % device_path, str(self))
+ with self._CopyToReadableLocation(device_path) as readable_temp_file:
+ self.adb.Pull(readable_temp_file.name, host_path)
+ else:
+ self.adb.Pull(device_path, host_path)
def _ReadFileWithPull(self, device_path):
try:
@@ -1936,12 +2043,8 @@ class DeviceUtils(object):
return _JoinLines(self.RunShellCommand(
['cat', device_path], as_root=as_root, check_return=True))
elif as_root and self.NeedsSU():
- with device_temp_file.DeviceTempFile(self.adb) as device_temp:
- 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, shell=True, as_root=True, check_return=True)
- return self._ReadFileWithPull(device_temp.name)
+ with self._CopyToReadableLocation(device_path) as readable_temp_file:
+ return self._ReadFileWithPull(readable_temp_file.name)
else:
return self._ReadFileWithPull(device_path)
@@ -2216,20 +2319,42 @@ class DeviceUtils(object):
else:
return False
+ def GetLocale(self, cache=False):
+ """Returns the locale setting on the device.
+
+ Args:
+ cache: Whether to use cached properties when available.
+ Returns:
+ A pair (language, country).
+ """
+ locale = self.GetProp('persist.sys.locale', cache=cache)
+ if locale:
+ if '-' not in locale:
+ logging.error('Unparsable locale: %s', locale)
+ return ('', '') # Behave as if persist.sys.locale is undefined.
+ return tuple(locale.split('-', 1))
+ return (self.GetProp('persist.sys.language', cache=cache),
+ self.GetProp('persist.sys.country', cache=cache))
+
def GetLanguage(self, cache=False):
"""Returns the language setting on the device.
+
+ DEPRECATED: Prefer GetLocale() instead.
+
Args:
cache: Whether to use cached properties when available.
"""
- return self.GetProp('persist.sys.language', cache=cache)
+ return self.GetLocale(cache=cache)[0]
def GetCountry(self, cache=False):
"""Returns the country setting on the device.
+ DEPRECATED: Prefer GetLocale() instead.
+
Args:
cache: Whether to use cached properties when available.
"""
- return self.GetProp('persist.sys.country', cache=cache)
+ return self.GetLocale(cache=cache)[1]
@property
def screen_density(self):
@@ -2302,7 +2427,11 @@ class DeviceUtils(object):
@property
def product_cpu_abi(self):
- """Returns the product cpu abi of the device (e.g. 'armeabi-v7a')."""
+ """Returns the product cpu abi of the device (e.g. 'armeabi-v7a').
+
+ For supported ABIs, the return value will be one of the values defined in
+ devil.android.ndk.abis.
+ """
return self.GetProp('ro.product.cpu.abi', cache=True)
@property
@@ -2428,7 +2557,8 @@ class DeviceUtils(object):
retries: number of retries
Returns:
- The device's main ABI name.
+ The device's main ABI name. For supported ABIs, the return value will be
+ one of the values defined in devil.android.ndk.abis.
Raises:
CommandTimeoutError on timeout.
@@ -2625,6 +2755,69 @@ class DeviceUtils(object):
check_return=True)
@decorators.WithTimeoutAndRetriesFromInstance()
+ def GetWebViewUpdateServiceDump(self, timeout=None, retries=None):
+ """Get the WebView update command sysdump on the device.
+
+ Returns:
+ A dictionary with these possible entries:
+ FallbackLogicEnabled: True|False
+ CurrentWebViewPackage: "package name" or None
+ MinimumWebViewVersionCode: int
+ WebViewPackages: Dict of installed WebView providers, mapping "package
+ name" to "reason it's valid/invalid."
+
+ It may return an empty dictionary if device does not
+ support the "dumpsys webviewupdate" command.
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+ """
+ result = {}
+
+ # Command was implemented starting in Oreo
+ if self.build_version_sdk < version_codes.OREO:
+ return result
+
+ output = self.RunShellCommand(
+ ['dumpsys', 'webviewupdate'], check_return=True)
+ webview_packages = {}
+ for line in output:
+ match = re.search(_WEBVIEW_SYSUPDATE_CURRENT_PKG_RE, line)
+ if match:
+ result['CurrentWebViewPackage'] = match.group(1)
+ match = re.search(_WEBVIEW_SYSUPDATE_NULL_PKG_RE, line)
+ if match:
+ result['CurrentWebViewPackage'] = None
+ match = re.search(_WEBVIEW_SYSUPDATE_FALLBACK_LOGIC_RE, line)
+ if match:
+ result['FallbackLogicEnabled'] = \
+ True if match.group(1) == 'true' else False
+ match = re.search(_WEBVIEW_SYSUPDATE_PACKAGE_INSTALLED_RE, line)
+ if match:
+ package_name = match.group(1)
+ reason = match.group(2)
+ webview_packages[package_name] = reason
+ match = re.search(_WEBVIEW_SYSUPDATE_PACKAGE_NOT_INSTALLED_RE, line)
+ if match:
+ package_name = match.group(1)
+ reason = match.group(2)
+ webview_packages[package_name] = reason
+ match = re.search(_WEBVIEW_SYSUPDATE_MIN_VERSION_CODE, line)
+ if match:
+ result['MinimumWebViewVersionCode'] = int(match.group(1))
+ if webview_packages:
+ result['WebViewPackages'] = webview_packages
+
+ missing_fields = set(['CurrentWebViewPackage', 'FallbackLogicEnabled']) - \
+ set(result.keys())
+ if len(missing_fields) > 0:
+ raise device_errors.CommandFailedError(
+ '%s not found in dumpsys webviewupdate' % str(list(missing_fields)))
+ return result
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def SetWebViewImplementation(self, package_name, timeout=None, retries=None):
"""Select the WebView implementation to the specified package.
@@ -2639,16 +2832,104 @@ class DeviceUtils(object):
CommandTimeoutError on timeout.
DeviceUnreachableError on missing device.
"""
+ installed = self.GetApplicationPaths(package_name)
+ if not installed:
+ raise device_errors.CommandFailedError(
+ '%s is not installed' % package_name, str(self))
output = self.RunShellCommand(
['cmd', 'webviewupdate', 'set-webview-implementation', package_name],
- single_line=True, check_return=True)
+ single_line=True,
+ check_return=False)
if output == 'Success':
logging.info('WebView provider set to: %s', package_name)
else:
+ dumpsys_output = self.GetWebViewUpdateServiceDump()
+ webview_packages = dumpsys_output.get('WebViewPackages')
+ if webview_packages:
+ reason = webview_packages.get(package_name)
+ if not reason:
+ all_provider_package_names = webview_packages.keys()
+ raise device_errors.CommandFailedError(
+ '%s is not in the system WebView provider list. Must choose one '
+ 'of %r.' % (package_name, all_provider_package_names), str(self))
+ if re.search(r'is\s+NOT\s+installed/enabled for all users', reason):
+ raise device_errors.CommandFailedError(
+ '%s is disabled, make sure to disable WebView fallback logic' %
+ package_name, str(self))
+ if re.search(r'No WebView-library manifest flag', reason):
+ raise device_errors.CommandFailedError(
+ '%s does not declare a WebView native library, so it cannot '
+ 'be a WebView provider' % package_name, str(self))
+ if re.search(r'SDK version too low', reason):
+ raise device_errors.CommandFailedError(
+ '%s needs a higher targetSdkVersion (must be >= %d)' %
+ (package_name, self.build_version_sdk), str(self))
+ if re.search(r'Version code too low', reason):
+ raise device_errors.CommandFailedError(
+ '%s needs a higher versionCode (must be >= %d)' %
+ (package_name, dumpsys_output.get('MinimumWebViewVersionCode')),
+ str(self))
+ if re.search(r'Incorrect signature', reason):
+ raise device_errors.CommandFailedError(
+ '%s is not signed with release keys (but user builds require '
+ 'this for WebView providers)' % package_name, str(self))
raise device_errors.CommandFailedError(
'Error setting WebView provider: %s' % output, str(self))
@decorators.WithTimeoutAndRetriesFromInstance()
+ def SetWebViewFallbackLogic(self, enabled, timeout=None, retries=None):
+ """Set whether WebViewUpdateService's "fallback logic" should be enabled.
+
+ WebViewUpdateService has nonintuitive "fallback logic" for devices where
+ Monochrome (Chrome Stable) is preinstalled as the WebView provider, with a
+ "stub" (little-to-no code) implementation of standalone WebView.
+
+ "Fallback logic" (enabled by default) is designed, in the case where the
+ user has disabled Chrome, to fall back to the stub standalone WebView by
+ enabling the package. The implementation plumbs through the Chrome APK until
+ Play Store installs an update with the full implementation.
+
+ A surprising side-effect of "fallback logic" is that, immediately after
+ sideloading WebView, WebViewUpdateService re-disables the package and
+ uninstalls the update. This can prevent successfully using standalone
+ WebView for development, although "fallback logic" can be disabled on
+ userdebug/eng devices.
+
+ Because this is only relevant for devices with the standalone WebView stub,
+ this command is only relevant on N-P (inclusive).
+
+ You can determine if "fallback logic" is currently enabled by checking
+ FallbackLogicEnabled in the dictionary returned by
+ GetWebViewUpdateServiceDump.
+
+ Args:
+ enabled: bool - True for enabled, False for disabled
+ timeout: timeout in seconds
+ retries: number of retries
+
+ Raises:
+ CommandFailedError on failure.
+ CommandTimeoutError on timeout.
+ DeviceUnreachableError on missing device.
+ """
+
+ # Command is only available on devices which preinstall stub WebView.
+ if not version_codes.NOUGAT <= self.build_version_sdk <= version_codes.PIE:
+ return
+
+ # redundant-packages is the opposite of fallback logic
+ enable_string = 'disable' if enabled else 'enable'
+ output = self.RunShellCommand(
+ ['cmd', 'webviewupdate', '%s-redundant-packages' % enable_string],
+ single_line=True, check_return=True)
+ if output == 'Success':
+ logging.info('WebView Fallback Logic is %s',
+ 'enabled' if enabled else 'disabled')
+ else:
+ raise device_errors.CommandFailedError(
+ 'Error setting WebView Fallback Logic: %s' % output, str(self))
+
+ @decorators.WithTimeoutAndRetriesFromInstance()
def TakeScreenshot(self, host_path=None, timeout=None, retries=None):
"""Takes a screenshot of the device.
@@ -2722,7 +3003,7 @@ class DeviceUtils(object):
self._client_caches[client_name] = {}
return self._client_caches[client_name]
- def _ClearCache(self):
+ def ClearCache(self):
"""Clears all caches."""
for client in self._client_caches:
self._client_caches[client].clear()
@@ -2826,7 +3107,7 @@ class DeviceUtils(object):
@classmethod
def HealthyDevices(cls, blacklist=None, device_arg='default', retries=1,
- abis=None, **kwargs):
+ enable_usb_resets=False, abis=None, **kwargs):
"""Returns a list of DeviceUtils instances.
Returns a list of DeviceUtils instances that are attached, not blacklisted,
@@ -2851,8 +3132,11 @@ class DeviceUtils(object):
retries: Number of times to restart adb server and query it again if no
devices are found on the previous attempts, with exponential backoffs
up to 60s between each retry.
+ enable_usb_resets: If true, will attempt to trigger a USB reset prior to
+ the last attempt if there are no available devices. It will only reset
+ those that appear to be android devices.
abis: A list of ABIs for which the device needs to support at least one of
- (optional).
+ (optional). See devil.android.ndk.abis for valid values.
A device serial, or a list of device serials (optional).
Returns:
@@ -2912,6 +3196,18 @@ class DeviceUtils(object):
raise device_errors.MultipleDevicesError(devices)
return sorted(devices)
+ def _reset_devices():
+ if not reset_usb:
+ logging.error(
+ 'reset_usb.py not supported on this platform (%s). Skipping usb '
+ 'resets.', sys.platform)
+ return
+ if device_arg:
+ for serial in device_arg:
+ reset_usb.reset_android_usb(serial)
+ else:
+ reset_usb.reset_all_android_devices()
+
for attempt in xrange(retries+1):
try:
return _get_devices()
@@ -2919,6 +3215,11 @@ class DeviceUtils(object):
if attempt == retries:
logging.error('No devices found after exhausting all retries.')
raise
+ elif attempt == retries - 1 and enable_usb_resets:
+ logging.warning(
+ 'Attempting to reset relevant USB devices prior to the last '
+ 'attempt.')
+ _reset_devices()
# math.pow returns floats, so cast to int for easier testing
sleep_s = min(int(math.pow(2, attempt + 1)), 60)
logger.warning(
diff --git a/catapult/devil/devil/android/device_utils_test.py b/catapult/devil/devil/android/device_utils_test.py
index e0ed666c..5799c7b8 100755
--- a/catapult/devil/devil/android/device_utils_test.py
+++ b/catapult/devil/devil/android/device_utils_test.py
@@ -15,12 +15,14 @@ import json
import logging
import os
import stat
+import sys
import unittest
from devil import devil_env
from devil.android import device_errors
from devil.android import device_signal
from devil.android import device_utils
+from devil.android.ndk import abis
from devil.android.sdk import adb_wrapper
from devil.android.sdk import intent
from devil.android.sdk import keyevent
@@ -31,9 +33,6 @@ from devil.utils import mock_calls
with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock # pylint: disable=import-error
-ARM32_ABI = 'armeabi-v7a'
-ARM64_ABI = 'arm64-v8a'
-
def Process(name, pid, ppid='1'):
return device_utils.ProcessInfo(name=name, pid=pid, ppid=ppid)
@@ -57,9 +56,10 @@ class _MockApkHelper(object):
def __init__(self, path, package_name, perms=None):
self.path = path
+ self.is_bundle = path.endswith('_bundle')
self.package_name = package_name
self.perms = perms
- self.abis = [ARM32_ABI]
+ self.abis = [abis.ARM]
def GetPackageName(self):
return self.package_name
@@ -314,36 +314,62 @@ class DeviceUtilsIsOnlineTest(DeviceUtilsTest):
class DeviceUtilsHasRootTest(DeviceUtilsTest):
def testHasRoot_true(self):
- with self.patch_call(self.call.device.product_name,
- return_value='notasailfish'), (
- self.assertCall(self.call.adb.Shell(
- 'ls /root', ensure_logs_on_timeout=False), 'foo\n')):
+ with self.patch_call(self.call.device.build_type,
+ return_value='userdebug'), (
+ 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',
- ensure_logs_on_timeout=False), '1\n')):
+ with self.patch_call(self.call.device.build_type,
+ return_value='userdebug'), (
+ 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 testhasRootSpecialAosp_true(self):
+ with self.patch_call(self.call.device.build_type,
+ return_value='userdebug'), (
+ self.patch_call(self.call.device.product_name,
+ return_value='aosp_sailfish')), (
+ self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+ '1\n')):
+ self.assertTrue(self.device.HasRoot())
+
+ def testhasRootEngBuild_true(self):
+ with self.patch_call(self.call.device.build_type,
+ return_value='eng'):
self.assertTrue(self.device.HasRoot())
def testHasRoot_false(self):
- with self.patch_call(self.call.device.product_name,
- return_value='notasailfish'), (
- self.assertCall(
- self.call.adb.Shell(
- 'ls /root', ensure_logs_on_timeout=False), self.ShellError())):
+ with self.patch_call(self.call.device.build_type,
+ return_value='userdebug'), (
+ 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', ensure_logs_on_timeout=False), '\n')):
+ with self.patch_call(self.call.device.build_type,
+ return_value='userdebug'), (
+ 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())
+ def testHasRootSpecialAosp_false(self):
+ with self.patch_call(self.call.device.build_type,
+ return_value='userdebug'), (
+ self.patch_call(self.call.device.product_name,
+ return_value='aosp_sailfish')), (
+ self.assertCall(self.call.adb.Shell('getprop service.adb.root'),
+ '\n')):
+ self.assertFalse(self.device.HasRoot())
class DeviceUtilsEnableRootTest(DeviceUtilsTest):
@@ -351,7 +377,7 @@ class DeviceUtilsEnableRootTest(DeviceUtilsTest):
with self.assertCalls(
self.call.adb.Root(),
self.call.adb.WaitForDevice(),
- (self.call.device.GetProp('service.adb.root', cache=False), '1')):
+ (self.call.device.HasRoot(), True)):
self.device.EnableRoot()
def testEnableRoot_userBuild(self):
@@ -368,6 +394,16 @@ class DeviceUtilsEnableRootTest(DeviceUtilsTest):
with self.assertRaises(device_errors.AdbCommandFailedError):
self.device.EnableRoot()
+ def testEnableRoot_timeoutInWaitForDevice(self):
+ with self.assertCalls(
+ (self.call.adb.Root(),
+ self.AdbCommandError(
+ output='timeout expired while waiting for device')),
+ (self.call.device.IsUserBuild(), False),
+ self.call.adb.WaitForDevice(),
+ (self.call.device.HasRoot(), True)):
+ self.device.EnableRoot()
+
class DeviceUtilsIsUserBuildTest(DeviceUtilsTest):
@@ -449,8 +485,7 @@ class DeviceUtils_GetApplicationVersionTest(DeviceUtilsTest):
def test_GetApplicationVersion_exists(self):
with self.assertCalls(
- (self.call.adb.Shell(
- 'dumpsys package com.android.chrome', ensure_logs_on_timeout=False),
+ (self.call.adb.Shell('dumpsys package com.android.chrome'),
'Packages:\n'
' Package [com.android.chrome] (3901ecfb):\n'
' userId=1234 gids=[123, 456, 789]\n'
@@ -461,16 +496,13 @@ class DeviceUtils_GetApplicationVersionTest(DeviceUtilsTest):
def test_GetApplicationVersion_notExists(self):
with self.assertCalls(
- (self.call.adb.Shell(
- 'dumpsys package com.android.chrome', ensure_logs_on_timeout=False),
- '')):
+ (self.call.adb.Shell('dumpsys package com.android.chrome'), '')):
self.assertEquals(None,
self.device.GetApplicationVersion('com.android.chrome'))
def test_GetApplicationVersion_fails(self):
with self.assertCalls(
- (self.call.adb.Shell(
- 'dumpsys package com.android.chrome', ensure_logs_on_timeout=False),
+ (self.call.adb.Shell('dumpsys package com.android.chrome'),
'Packages:\n'
' Package [com.android.chrome] (3901ecfb):\n'
' userId=1234 gids=[123, 456, 789]\n'
@@ -487,7 +519,7 @@ class DeviceUtils_GetPackageArchitectureTest(DeviceUtilsTest):
'dumpsys package com.android.chrome | grep -F primaryCpuAbi'),
[' primaryCpuAbi=armeabi-v7a']):
self.assertEquals(
- ARM32_ABI,
+ abis.ARM,
self.device.GetPackageArchitecture('com.android.chrome'))
def test_GetPackageArchitecture_notExists(self):
@@ -528,8 +560,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -543,8 +574,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -552,8 +582,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
# boot_completed
(self.call.device.GetProp('sys.boot_completed', cache=False), '1'),
# wifi_enabled
- (self.call.adb.Shell(
- 'dumpsys wifi', ensure_logs_on_timeout=False),
+ (self.call.adb.Shell('dumpsys wifi'),
'stuff\nWi-Fi is enabled\nmore stuff\n')):
self.device.WaitUntilFullyBooted(wifi=True)
@@ -570,8 +599,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
(self.call.device.GetExternalStoragePath(), self.AdbCommandError()),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -585,8 +613,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -611,18 +638,13 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False),
- self.ShellError()),
+ (self.call.adb.Shell('test -d /fake/storage/path'), self.ShellError()),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False),
- self.ShellError()),
+ (self.call.adb.Shell('test -d /fake/storage/path'), self.ShellError()),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False),
+ (self.call.adb.Shell('test -d /fake/storage/path'),
self.TimeoutError())):
with self.assertRaises(device_errors.CommandTimeoutError):
self.device.WaitUntilFullyBooted(wifi=False)
@@ -632,8 +654,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -654,8 +675,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -675,8 +695,7 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
self.call.adb.WaitForDevice(),
# sd_card_ready
(self.call.device.GetExternalStoragePath(), '/fake/storage/path'),
- (self.call.adb.Shell(
- 'test -d /fake/storage/path', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('test -d /fake/storage/path'), ''),
# pm_ready
(self.call.device._GetApplicationPathsInternal('android',
skip_cache=True),
@@ -684,14 +703,11 @@ class DeviceUtilsWaitUntilFullyBootedTest(DeviceUtilsTest):
# boot_completed
(self.call.device.GetProp('sys.boot_completed', cache=False), '1'),
# wifi_enabled
- (self.call.adb.Shell(
- 'dumpsys wifi', ensure_logs_on_timeout=False), 'stuff\nmore stuff\n'),
+ (self.call.adb.Shell('dumpsys wifi'), 'stuff\nmore stuff\n'),
# wifi_enabled
- (self.call.adb.Shell(
- 'dumpsys wifi', ensure_logs_on_timeout=False), 'stuff\nmore stuff\n'),
+ (self.call.adb.Shell('dumpsys wifi'), 'stuff\nmore stuff\n'),
# wifi_enabled
- (self.call.adb.Shell(
- 'dumpsys wifi', ensure_logs_on_timeout=False), self.TimeoutError())):
+ (self.call.adb.Shell('dumpsys wifi'), self.TimeoutError())):
with self.assertRaises(device_errors.CommandTimeoutError):
self.device.WaitUntilFullyBooted(wifi=True)
@@ -766,6 +782,18 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
permissions=['p1', 'p2'])
+ def testInstall_identicalPriorInstall(self):
+ with self.assertCalls(
+ (mock.call.os.path.exists('/fake/test/app.apk'), True),
+ (self.call.device._GetApplicationPathsInternal('test.package'),
+ ['/fake/data/app/test.package.apk']),
+ (self.call.device._ComputeStaleApks('test.package',
+ ['/fake/test/app.apk']),
+ ([], None)),
+ (self.call.device.ForceStop('test.package'))):
+ self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
+ permissions=[])
+
def testInstall_differentPriorInstall(self):
with self.assertCalls(
(mock.call.os.path.exists('/fake/test/app.apk'), True),
@@ -780,6 +808,18 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
permissions=[])
+ def testInstall_differentPriorInstallSplitApk(self):
+ with self.assertCalls(
+ (mock.call.os.path.exists('/fake/test/app.apk'), True),
+ (self.call.device._GetApplicationPathsInternal('test.package'),
+ ['/fake/data/app/test.package.apk',
+ '/fake/data/app/test.package2.apk']),
+ self.call.device.Uninstall('test.package'),
+ self.call.adb.Install('/fake/test/app.apk', reinstall=False,
+ allow_downgrade=False)):
+ self.device.Install(DeviceUtilsInstallTest.mock_apk, retries=0,
+ permissions=[])
+
def testInstall_differentPriorInstall_reinstall(self):
with self.assertCalls(
(mock.call.os.path.exists('/fake/test/app.apk'), True),
@@ -834,6 +874,11 @@ class DeviceUtilsInstallTest(DeviceUtilsTest):
self.device.Install(DeviceUtilsInstallTest.mock_apk,
reinstall=True, retries=0, permissions=[], allow_downgrade=True)
+ def testInstall_modulesSpecified(self):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.Install(DeviceUtilsInstallTest.mock_apk,
+ modules=['base'])
+
class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
@@ -916,6 +961,61 @@ class DeviceUtilsInstallSplitApkTest(DeviceUtilsTest):
['split1.apk', 'split2.apk', 'split3.apk'], permissions=[],
retries=0)
+ def testInstallSplitApk_previouslyNonSplit(self):
+ with self.assertCalls(
+ (self.call.device._CheckSdkLevel(21)),
+ (mock.call.devil.android.sdk.split_select.SelectSplits(
+ self.device, 'base.apk',
+ ['split1.apk', 'split2.apk', 'split3.apk'],
+ allow_cached_props=False),
+ ['split2.apk']),
+ (mock.call.os.path.exists('base.apk'), True),
+ (mock.call.os.path.exists('split2.apk'), True),
+ (self.call.device._GetApplicationPathsInternal(
+ 'test.package'), ['/fake/data/app/test.package.apk']),
+ self.call.device.Uninstall('test.package'),
+ (self.call.adb.InstallMultiple(
+ ['base.apk', 'split2.apk'], partial=None, reinstall=False,
+ allow_downgrade=False))):
+ self.device.InstallSplitApk(DeviceUtilsInstallSplitApkTest.mock_apk,
+ ['split1.apk', 'split2.apk', 'split3.apk'], permissions=[], retries=0)
+
+
+class DeviceUtilsInstallBundleTest(DeviceUtilsTest):
+ mock_apk = _MockApkHelper('/fake/test/app_bundle', 'test.package', ['p1'])
+
+ def testInstallBundle_noPriorInstall(self):
+ with self.patch_call(self.call.device.build_version_sdk, return_value=23):
+ with self.assertCalls(
+ (mock.call.devil.utils.cmd_helper.RunCmd(
+ ['/fake/test/app_bundle', 'install', '--device',
+ self.device.serial]), 0),
+ (self.call.device.GrantPermissions('test.package', ['p1']), [])):
+ self.device.Install(DeviceUtilsInstallBundleTest.mock_apk)
+
+ def testInstallBundle_modulesSpecified(self):
+ with self.patch_call(self.call.device.build_version_sdk, return_value=23):
+ with self.assertCalls(
+ (mock.call.devil.utils.cmd_helper.RunCmd(
+ ['/fake/test/app_bundle', 'install', '--device',
+ self.device.serial, '-m', 'base']), 0),
+ (self.call.device.GrantPermissions('test.package', ['p1']), [])):
+ self.device.Install(
+ DeviceUtilsInstallBundleTest.mock_apk, modules=['base'])
+
+ def testInstallBundle_permissionsPreM(self):
+ with self.patch_call(self.call.device.build_version_sdk, return_value=20):
+ with self.assertCalls(
+ (mock.call.devil.utils.cmd_helper.RunCmd(
+ ['/fake/test/app_bundle', 'install', '--device',
+ self.device.serial]), 0)):
+ self.device.Install(DeviceUtilsInstallBundleTest.mock_apk)
+
+ def testInstallBundle_splitApks(self):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.InstallSplitApk(
+ DeviceUtilsInstallBundleTest.mock_apk, ['apk1', 'apk2'])
+
class DeviceUtilsUninstallTest(DeviceUtilsTest):
@@ -954,36 +1054,30 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
self.device.NeedsSU = mock.Mock(return_value=False)
def testRunShellCommand_commandAsList(self):
- with self.assertCall(self.call.adb.Shell(
- 'pm list packages', ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell('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'", ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell("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"', ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell('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"', ensure_logs_on_timeout=False), ''):
+ self.call.adb.Shell('VAR=some_string echo "$VAR"'), ''):
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', ensure_logs_on_timeout=False),
- ''):
+ self.call.adb.Shell('PATH="$PATH:/other/path" run_this'), ''):
self.device.RunShellCommand(
['run_this'], check_return=True, env={'PATH': '$PATH:/other/path'})
@@ -993,17 +1087,13 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
['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', ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell('cd /some/test/path && ls'), ''):
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",
- ensure_logs_on_timeout=False),
- ''):
+ self.call.adb.Shell("cd '/some test/path with/spaces' && ls"), ''):
self.device.RunShellCommand(
['ls'], check_return=True, cwd='/some test/path with/spaces')
@@ -1014,9 +1104,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
(mock.call.devil.android.device_temp_file.DeviceTempFile(
self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
- (self.call.adb.Shell(
- 'sh /sdcard/temp-123.sh', ensure_logs_on_timeout=False),
- payload + '\n')):
+ (self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
self.assertEquals(
[payload],
self.device.RunShellCommand(['echo', payload], check_return=True))
@@ -1031,9 +1119,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
(mock.call.devil.android.device_temp_file.DeviceTempFile(
self.adb, suffix='.sh'), MockTempFile('/sdcard/temp-123.sh')),
self.call.device._WriteFileWithPush('/sdcard/temp-123.sh', expected_cmd),
- (self.call.adb.Shell(
- 'sh /sdcard/temp-123.sh', ensure_logs_on_timeout=False),
- payload + '\n')):
+ (self.call.adb.Shell('sh /sdcard/temp-123.sh'), payload + '\n')):
self.assertEquals(
[payload],
self.device.RunShellCommand(
@@ -1045,8 +1131,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.NeedsSU(), True),
(self.call.device._Su(expected_cmd_without_su), expected_cmd),
- (self.call.adb.Shell(
- expected_cmd, ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell(expected_cmd), '')):
self.device.RunShellCommand(
['setprop', 'service.adb.root', '0'],
check_return=True, as_root=True)
@@ -1055,8 +1140,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
expected_cmd_without_run_as = "sh -c 'mkdir -p files'"
expected_cmd = (
'run-as org.devil.test_package %s' % expected_cmd_without_run_as)
- with self.assertCall(self.call.adb.Shell(
- expected_cmd, ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell(expected_cmd), ''):
self.device.RunShellCommand(
['mkdir', '-p', 'files'],
check_return=True, run_as='org.devil.test_package')
@@ -1071,8 +1155,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.NeedsSU(), True),
(self.call.device._Su(expected_cmd_without_su), expected_cmd),
- (self.call.adb.Shell(
- expected_cmd, ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell(expected_cmd), '')):
self.device.RunShellCommand(
['mkdir', '-p', 'files'],
check_return=True, run_as='org.devil.test_package',
@@ -1080,16 +1163,14 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_manyLines(self):
cmd = 'ls /some/path'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), 'file1\nfile2\nfile3\n'):
+ with self.assertCall(self.call.adb.Shell(cmd), 'file1\nfile2\nfile3\n'):
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, ensure_logs_on_timeout=False), '\rfile1\nfile2\r\nfile3\n'):
+ with self.assertCall(self.call.adb.Shell(cmd), '\rfile1\nfile2\r\nfile3\n'):
self.assertEquals(
'\rfile1\nfile2\r\nfile3\n',
self.device.RunShellCommand(
@@ -1097,8 +1178,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_singleLine_success(self):
cmd = 'echo $VALUE'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), 'some value\n'):
+ with self.assertCall(self.call.adb.Shell(cmd), 'some value\n'):
self.assertEquals(
'some value',
self.device.RunShellCommand(
@@ -1106,8 +1186,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_singleLine_successEmptyLine(self):
cmd = 'echo $VALUE'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), '\n'):
+ with self.assertCall(self.call.adb.Shell(cmd), '\n'):
self.assertEquals(
'',
self.device.RunShellCommand(
@@ -1115,8 +1194,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_singleLine_successWithoutEndLine(self):
cmd = 'echo -n $VALUE'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), 'some value'):
+ with self.assertCall(self.call.adb.Shell(cmd), 'some value'):
self.assertEquals(
'some value',
self.device.RunShellCommand(
@@ -1124,8 +1202,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_singleLine_successNoOutput(self):
cmd = 'echo -n $VALUE'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell(cmd), ''):
self.assertEquals(
'',
self.device.RunShellCommand(
@@ -1133,8 +1210,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_singleLine_failTooManyLines(self):
cmd = 'echo $VALUE'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False),
+ with self.assertCall(self.call.adb.Shell(cmd),
'some value\nanother value\n'):
with self.assertRaises(device_errors.CommandFailedError):
self.device.RunShellCommand(
@@ -1143,8 +1219,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_checkReturn_success(self):
cmd = 'echo $ANDROID_DATA'
output = '/data\n'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), output):
+ with self.assertCall(self.call.adb.Shell(cmd), output):
self.assertEquals(
[output.rstrip()],
self.device.RunShellCommand(cmd, shell=True, check_return=True))
@@ -1152,16 +1227,14 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_checkReturn_failure(self):
cmd = 'ls /root'
output = 'opendir failed, Permission denied\n'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), self.ShellError(output)):
+ with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)):
with self.assertRaises(device_errors.AdbCommandFailedError):
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, ensure_logs_on_timeout=False), self.ShellError(output)):
+ with self.assertCall(self.call.adb.Shell(cmd), self.ShellError(output)):
self.assertEquals(
[output.rstrip()],
self.device.RunShellCommand(cmd.split(), check_return=False))
@@ -1173,7 +1246,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
with self.assertCalls(
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
temp_file),
- (self.call.adb.Shell(cmd_redirect, ensure_logs_on_timeout=False)),
+ (self.call.adb.Shell(cmd_redirect)),
(self.call.device.ReadFile(temp_file.name, force_pull=True),
'something')):
self.assertEquals(
@@ -1183,8 +1256,7 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
def testRunShellCommand_largeOutput_disabledNoTrigger(self):
cmd = 'something'
- with self.assertCall(self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), self.ShellError('')):
+ with self.assertCall(self.call.adb.Shell(cmd), self.ShellError('')):
with self.assertRaises(device_errors.AdbCommandFailedError):
self.device.RunShellCommand([cmd], check_return=True)
@@ -1193,12 +1265,10 @@ class DeviceUtilsRunShellCommandTest(DeviceUtilsTest):
temp_file = MockTempFile('/sdcard/temp-123')
cmd_redirect = '( %s )>%s 2>&1' % (cmd, temp_file.name)
with self.assertCalls(
- (self.call.adb.Shell(
- cmd, ensure_logs_on_timeout=False), self.ShellError('', None)),
+ (self.call.adb.Shell(cmd), self.ShellError('', None)),
(mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
temp_file),
- (self.call.adb.Shell(
- cmd_redirect, ensure_logs_on_timeout=False)),
+ (self.call.adb.Shell(cmd_redirect)),
(self.call.device.ReadFile(mock.ANY, force_pull=True),
'something')):
self.assertEquals(
@@ -1264,8 +1334,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234), ('some.process.thing', 5678))),
- (self.call.adb.Shell(
- 'kill -9 1234 5678', ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell('kill -9 1234 5678'), '')):
self.assertEquals(
2, self.device.KillAll('some.process', blocking=False))
@@ -1273,8 +1342,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234), ('some.process.thing', 5678))),
- (self.call.adb.Shell(
- 'kill -9 1234 5678', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('kill -9 1234 5678'), ''),
(self.call.device.ListProcesses('some.process'),
Processes(('some.process.thing', 5678))),
(self.call.device.ListProcesses('some.process'),
@@ -1287,8 +1355,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234), ('some.process.thing', 5678))),
- (self.call.adb.Shell(
- 'kill -9 1234', ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell('kill -9 1234'), '')):
self.assertEquals(
1, self.device.KillAll('some.process', exact=True, blocking=False))
@@ -1296,8 +1363,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234), ('some.process.thing', 5678))),
- (self.call.adb.Shell(
- 'kill -9 1234', ensure_logs_on_timeout=False), ''),
+ (self.call.adb.Shell('kill -9 1234'), ''),
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234), ('some.process.thing', 5678))),
(self.call.device.ListProcesses('some.process'),
@@ -1312,8 +1378,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
(self.call.device.NeedsSU(), True),
(self.call.device._Su("sh -c 'kill -9 1234'"),
"su -c sh -c 'kill -9 1234'"),
- (self.call.adb.Shell(
- "su -c sh -c 'kill -9 1234'", ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell("su -c sh -c 'kill -9 1234'"), '')):
self.assertEquals(
1, self.device.KillAll('some.process', as_root=True))
@@ -1321,8 +1386,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234))),
- (self.call.adb.Shell(
- 'kill -15 1234', ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell('kill -15 1234'), '')):
self.assertEquals(
1, self.device.KillAll('some.process', signum=device_signal.SIGTERM))
@@ -1330,8 +1394,7 @@ class DeviceUtilsKillAllTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.ListProcesses('some.process'),
Processes(('some.process', 1234), ('some.process', 4567))),
- (self.call.adb.Shell(
- 'kill -15 1234 4567', ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell('kill -15 1234 4567'), '')):
self.assertEquals(
2, self.device.KillAll('some.process', signum=device_signal.SIGTERM))
@@ -1341,10 +1404,8 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
def testStartActivity_actionOnly(self):
test_intent = intent.Intent(action='android.intent.action.VIEW')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1353,11 +1414,9 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
package='test.package',
activity='.Main')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1366,11 +1425,9 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
package='test.package',
activity='.Main')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main'),
'Error: Failed to start test activity'):
with self.assertRaises(device_errors.CommandFailedError):
self.device.StartActivity(test_intent)
@@ -1380,12 +1437,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
package='test.package',
activity='.Main')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-W '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-W '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent, blocking=True)
@@ -1395,12 +1450,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
activity='.Main',
category='android.intent.category.HOME')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-c android.intent.category.HOME '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-c android.intent.category.HOME '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1411,13 +1464,11 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
category=['android.intent.category.HOME',
'android.intent.category.BROWSABLE'])
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-c android.intent.category.HOME '
- '-c android.intent.category.BROWSABLE '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-c android.intent.category.HOME '
+ '-c android.intent.category.BROWSABLE '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1427,12 +1478,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
activity='.Main',
data='http://www.google.com/')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-d http://www.google.com/ '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-d http://www.google.com/ '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1442,12 +1491,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
activity='.Main',
extras={'foo': 'test'})
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main '
- '--es foo test',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main '
+ '--es foo test'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1457,12 +1504,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
activity='.Main',
extras={'foo': True})
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main '
- '--ez foo True',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main '
+ '--ez foo True'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1472,12 +1517,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
activity='.Main',
extras={'foo': 123})
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main '
- '--ei foo 123',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main '
+ '--ei foo 123'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1486,12 +1529,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
package='test.package',
activity='.Main')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '--start-profiler test_trace_file.out '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '--start-profiler test_trace_file.out '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent,
trace_file_name='test_trace_file.out')
@@ -1501,12 +1542,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
package='test.package',
activity='.Main')
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-S '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-S '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent, force_stop=True)
@@ -1519,12 +1558,10 @@ class DeviceUtilsStartActivityTest(DeviceUtilsTest):
intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
])
with self.assertCall(
- self.call.adb.Shell(
- 'am start '
- '-a android.intent.action.VIEW '
- '-n test.package/.Main '
- '-f 0x10200000',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start '
+ '-a android.intent.action.VIEW '
+ '-n test.package/.Main '
+ '-f 0x10200000'),
'Starting: Intent { act=android.intent.action.VIEW }'):
self.device.StartActivity(test_intent)
@@ -1537,11 +1574,9 @@ class DeviceUtilsStartServiceTest(DeviceUtilsTest):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.NOUGAT):
with self.assertCall(
- self.call.adb.Shell(
- 'am startservice '
- '-a android.intent.action.START '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am startservice '
+ '-a android.intent.action.START '
+ '-n test.package/.Main'),
'Starting service: Intent { act=android.intent.action.START }'):
self.device.StartService(test_intent)
@@ -1552,11 +1587,9 @@ class DeviceUtilsStartServiceTest(DeviceUtilsTest):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.NOUGAT):
with self.assertCall(
- self.call.adb.Shell(
- 'am startservice '
- '-a android.intent.action.START '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am startservice '
+ '-a android.intent.action.START '
+ '-n test.package/.Main'),
'Error: Failed to start test service'):
with self.assertRaises(device_errors.CommandFailedError):
self.device.StartService(test_intent)
@@ -1568,12 +1601,10 @@ class DeviceUtilsStartServiceTest(DeviceUtilsTest):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.NOUGAT):
with self.assertCall(
- self.call.adb.Shell(
- 'am startservice '
- '--user TestUser '
- '-a android.intent.action.START '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am startservice '
+ '--user TestUser '
+ '-a android.intent.action.START '
+ '-n test.package/.Main'),
'Starting service: Intent { act=android.intent.action.START }'):
self.device.StartService(test_intent, user_id='TestUser')
@@ -1584,11 +1615,9 @@ class DeviceUtilsStartServiceTest(DeviceUtilsTest):
with self.patch_call(self.call.device.build_version_sdk,
return_value=version_codes.OREO):
with self.assertCall(
- self.call.adb.Shell(
- 'am start-service '
- '-a android.intent.action.START '
- '-n test.package/.Main',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am start-service '
+ '-a android.intent.action.START '
+ '-n test.package/.Main'),
'Starting service: Intent { act=android.intent.action.START }'):
self.device.StartService(test_intent)
@@ -1641,9 +1670,7 @@ class DeviceUtilsBroadcastIntentTest(DeviceUtilsTest):
def testBroadcastIntent_noExtras(self):
test_intent = intent.Intent(action='test.package.with.an.INTENT')
with self.assertCall(
- self.call.adb.Shell(
- 'am broadcast -a test.package.with.an.INTENT',
- ensure_logs_on_timeout=False),
+ self.call.adb.Shell('am broadcast -a test.package.with.an.INTENT'),
'Broadcasting: Intent { act=test.package.with.an.INTENT } '):
self.device.BroadcastIntent(test_intent)
@@ -1652,8 +1679,7 @@ class DeviceUtilsBroadcastIntentTest(DeviceUtilsTest):
extras={'foo': 'bar value'})
with self.assertCall(
self.call.adb.Shell(
- "am broadcast -a test.package.with.an.INTENT --es foo 'bar value'",
- ensure_logs_on_timeout=False),
+ "am broadcast -a test.package.with.an.INTENT --es foo 'bar value'"),
'Broadcasting: Intent { act=test.package.with.an.INTENT } '):
self.device.BroadcastIntent(test_intent)
@@ -1662,8 +1688,7 @@ class DeviceUtilsBroadcastIntentTest(DeviceUtilsTest):
extras={'foo': None})
with self.assertCall(
self.call.adb.Shell(
- 'am broadcast -a test.package.with.an.INTENT --esn foo',
- ensure_logs_on_timeout=False),
+ 'am broadcast -a test.package.with.an.INTENT --esn foo'),
'Broadcasting: Intent { act=test.package.with.an.INTENT } '):
self.device.BroadcastIntent(test_intent)
@@ -1827,8 +1852,7 @@ class DeviceUtilsClearApplicationStateTest(DeviceUtilsTest):
class DeviceUtilsSendKeyEventTest(DeviceUtilsTest):
def testSendKeyEvent(self):
- with self.assertCall(self.call.adb.Shell(
- 'input keyevent 66', ensure_logs_on_timeout=False), ''):
+ with self.assertCall(self.call.adb.Shell('input keyevent 66'), ''):
self.device.SendKeyEvent(66)
@@ -2003,6 +2027,33 @@ class DeviceUtilsPullFileTest(DeviceUtilsTest):
self.device.PullFile('/data/app/test.file.does.not.exist',
'/test/file/host/path')
+ def testPullFile_asRoot(self):
+ with mock.patch('os.path.exists', return_value=True):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), True),
+ (self.call.device.PathExists('/this/file/can.be.read.with.su',
+ as_root=True), True),
+ (mock.call.devil.android.device_temp_file.DeviceTempFile(self.adb),
+ MockTempFile('/sdcard/tmp/on.device')),
+ self.call.device.RunShellCommand(
+ 'SRC=/this/file/can.be.read.with.su DEST=/sdcard/tmp/on.device;'
+ 'cp "$SRC" "$DEST" && chmod 666 "$DEST"',
+ shell=True, as_root=True, check_return=True),
+ (self.call.adb.Pull('/sdcard/tmp/on.device',
+ '/test/file/host/path'))):
+ self.device.PullFile('/this/file/can.be.read.with.su',
+ '/test/file/host/path', as_root=True)
+
+ def testPullFile_asRootDoesntExistOnDevice(self):
+ with mock.patch('os.path.exists', return_value=True):
+ with self.assertCalls(
+ (self.call.device.NeedsSU(), True),
+ (self.call.device.PathExists('/data/app/test.file.does.not.exist',
+ as_root=True), False)):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.PullFile('/data/app/test.file.does.not.exist',
+ '/test/file/host/path', as_root=True)
+
class DeviceUtilsReadFileTest(DeviceUtilsTest):
@@ -2167,14 +2218,12 @@ class DeviceUtilsWriteFileTest(DeviceUtilsTest):
def testWriteFile_withEcho(self):
with self.assertCall(self.call.adb.Shell(
- "echo -n the.contents > /test/file/to.write",
- ensure_logs_on_timeout=False), ''):
+ "echo -n the.contents > /test/file/to.write"), ''):
self.device.WriteFile('/test/file/to.write', 'the.contents')
def testWriteFile_withEchoAndQuotes(self):
with self.assertCall(self.call.adb.Shell(
- "echo -n 'the contents' > '/test/file/to write'",
- ensure_logs_on_timeout=False), ''):
+ "echo -n 'the contents' > '/test/file/to write'"), ''):
self.device.WriteFile('/test/file/to write', 'the contents')
def testWriteFile_withEchoAndSU(self):
@@ -2183,8 +2232,7 @@ class DeviceUtilsWriteFileTest(DeviceUtilsTest):
with self.assertCalls(
(self.call.device.NeedsSU(), True),
(self.call.device._Su(expected_cmd_without_su), expected_cmd),
- (self.call.adb.Shell(
- expected_cmd, ensure_logs_on_timeout=False),
+ (self.call.adb.Shell(expected_cmd),
'')):
self.device.WriteFile('/test/file', 'contents', as_root=True)
@@ -2665,77 +2713,251 @@ class DeviceUtilsListProcessesTest(DeviceUtilsTest):
class DeviceUtilsGetSetEnforce(DeviceUtilsTest):
def testGetEnforce_Enforcing(self):
- with self.assertCall(self.call.adb.Shell(
- 'getenforce', ensure_logs_on_timeout=False), 'Enforcing'):
+ 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', ensure_logs_on_timeout=False), 'Permissive'):
+ 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', ensure_logs_on_timeout=False), 'Disabled'):
+ 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', ensure_logs_on_timeout=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', ensure_logs_on_timeout=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', ensure_logs_on_timeout=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', ensure_logs_on_timeout=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', ensure_logs_on_timeout=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', ensure_logs_on_timeout=False), '')):
+ (self.call.adb.Shell('setenforce 0'), '')):
self.device.SetEnforce(enabled='0') # Not recommended but it works!
+class DeviceUtilsGetWebViewUpdateServiceDumpTest(DeviceUtilsTest):
+
+ def testGetWebViewUpdateServiceDump_success(self):
+ # Some of the lines of adb shell dumpsys webviewupdate:
+ dumpsys_lines = [
+ 'Fallback logic enabled: true',
+ ('Current WebView package (name, version): '
+ '(com.android.chrome, 61.0.3163.98)'),
+ 'Minimum WebView version code: 12345',
+ 'WebView packages:',
+ ('Valid package com.android.chrome (versionName: '
+ '61.0.3163.98, versionCode: 1, targetSdkVersion: 26) is '
+ 'installed/enabled for all users'),
+ ('Valid package com.google.android.webview (versionName: '
+ '58.0.3029.125, versionCode: 1, targetSdkVersion: 26) is NOT '
+ 'installed/enabled for all users'),
+ ('Invalid package com.google.android.apps.chrome (versionName: '
+ '56.0.2924.122, versionCode: 2, targetSdkVersion: 25), reason: SDK '
+ 'version too low'),
+ ('com.chrome.canary is NOT installed.'),
+ ]
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.OREO):
+ with self.assertCall(
+ self.call.adb.Shell('dumpsys webviewupdate'),
+ '\n'.join(dumpsys_lines)):
+ update = self.device.GetWebViewUpdateServiceDump()
+ self.assertTrue(update['FallbackLogicEnabled'])
+ self.assertEqual('com.android.chrome',
+ update['CurrentWebViewPackage'])
+ self.assertEqual(12345, update['MinimumWebViewVersionCode'])
+ # Order isn't really important, and we shouldn't have duplicates, so we
+ # convert to sets.
+ expected = {
+ 'com.android.chrome', 'com.google.android.webview',
+ 'com.google.android.apps.chrome', 'com.chrome.canary'
+ }
+ self.assertSetEqual(expected, set(update['WebViewPackages'].keys()))
+ self.assertEquals(
+ 'is installed/enabled for all users',
+ update['WebViewPackages']['com.android.chrome'])
+ self.assertEquals(
+ 'is NOT installed/enabled for all users',
+ update['WebViewPackages']['com.google.android.webview'])
+ self.assertEquals(
+ 'reason: SDK version too low',
+ update['WebViewPackages']['com.google.android.apps.chrome'])
+ self.assertEquals(
+ 'is NOT installed.',
+ update['WebViewPackages']['com.chrome.canary'])
+
+ def testGetWebViewUpdateServiceDump_missingkey(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.OREO):
+ with self.assertCall(self.call.adb.Shell('dumpsys webviewupdate'),
+ 'Fallback logic enabled: true'):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.GetWebViewUpdateServiceDump()
+
+ def testGetWebViewUpdateServiceDump_noop(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.NOUGAT_MR1):
+ with self.assertCalls():
+ self.device.GetWebViewUpdateServiceDump()
+
+ def testGetWebViewUpdateServiceDump_noPackage(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.OREO):
+ with self.assertCall(self.call.adb.Shell('dumpsys webviewupdate'),
+ 'Fallback logic enabled: true\n'
+ 'Current WebView package is null'):
+ update = self.device.GetWebViewUpdateServiceDump()
+ self.assertEqual(True, update['FallbackLogicEnabled'])
+ self.assertEqual(None, update['CurrentWebViewPackage'])
+
+
class DeviceUtilsSetWebViewImplementationTest(DeviceUtilsTest):
def testSetWebViewImplementation_success(self):
- with self.assertCall(self.call.adb.Shell(
- 'cmd webviewupdate set-webview-implementation foo.org',
- ensure_logs_on_timeout=False), 'Success'):
- self.device.SetWebViewImplementation('foo.org')
+ with self.patch_call(
+ self.call.device.GetApplicationPaths, return_value=['/any/path']):
+ with self.assertCall(
+ self.call.adb.Shell(
+ 'cmd webviewupdate set-webview-implementation foo.org'),
+ 'Success'):
+ self.device.SetWebViewImplementation('foo.org')
- def testSetWebViewImplementation_failure(self):
- with self.assertCall(self.call.adb.Shell(
- 'cmd webviewupdate set-webview-implementation foo.org',
- ensure_logs_on_timeout=False), 'Oops!'):
- with self.assertRaises(device_errors.CommandFailedError):
+ def testSetWebViewImplementation_uninstalled(self):
+ with self.patch_call(self.call.device.GetApplicationPaths, return_value=[]):
+ with self.assertRaises(device_errors.CommandFailedError) as cfe:
self.device.SetWebViewImplementation('foo.org')
+ self.assertIn('is not installed', cfe.exception.message)
+
+ def _testSetWebViewImplementationHelper(self, mock_dump_sys,
+ exception_message_substr):
+ with self.patch_call(
+ self.call.device.GetApplicationPaths, return_value=['/any/path']):
+ with self.assertCall(
+ self.call.adb.Shell(
+ 'cmd webviewupdate set-webview-implementation foo.org'), 'Oops!'):
+ with self.patch_call(
+ self.call.device.GetWebViewUpdateServiceDump,
+ return_value=mock_dump_sys):
+ with self.assertRaises(device_errors.CommandFailedError) as cfe:
+ self.device.SetWebViewImplementation('foo.org')
+ self.assertIn(exception_message_substr, cfe.exception.message)
+
+ def testSetWebViewImplementation_notInProviderList(self):
+ mock_dump_sys = {
+ 'WebViewPackages': {
+ 'some.package': 'any reason',
+ 'other.package': 'any reason',
+ }
+ }
+ self._testSetWebViewImplementationHelper(mock_dump_sys, 'provider list')
+
+ def testSetWebViewImplementation_notEnabled(self):
+ mock_dump_sys = {
+ 'WebViewPackages': {
+ 'foo.org': 'is NOT installed/enabled for all users',
+ }
+ }
+ self._testSetWebViewImplementationHelper(mock_dump_sys, 'is disabled')
+
+ def testSetWebViewImplementation_missingManifestTag(self):
+ mock_dump_sys = {
+ 'WebViewPackages': {
+ 'foo.org': 'No WebView-library manifest flag',
+ }
+ }
+ self._testSetWebViewImplementationHelper(mock_dump_sys,
+ 'WebView native library')
+
+ def testSetWebViewImplementation_lowTargetSdkVersion(self):
+ mock_dump_sys = {'WebViewPackages': {'foo.org': 'SDK version too low',}}
+ with self.patch_call(self.call.device.build_version_sdk, return_value=26):
+ self._testSetWebViewImplementationHelper(mock_dump_sys,
+ 'higher targetSdkVersion')
+
+ def testSetWebViewImplementation_lowVersionCode(self):
+ mock_dump_sys = {
+ 'MinimumWebViewVersionCode': 12345,
+ 'WebViewPackages': {
+ 'foo.org': 'Version code too low',
+ }
+ }
+ self._testSetWebViewImplementationHelper(mock_dump_sys,
+ 'higher versionCode')
+
+ def testSetWebViewImplementation_invalidSignature(self):
+ mock_dump_sys = {
+ 'WebViewPackages': {
+ 'foo.org': 'Incorrect signature',
+ }
+ }
+ self._testSetWebViewImplementationHelper(mock_dump_sys,
+ 'signed with release keys')
+
+
+class DeviceUtilsSetWebViewFallbackLogicTest(DeviceUtilsTest):
+
+ def testSetWebViewFallbackLogic_False_success(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.NOUGAT):
+ with self.assertCall(self.call.adb.Shell(
+ 'cmd webviewupdate enable-redundant-packages'), 'Success'):
+ self.device.SetWebViewFallbackLogic(False)
+
+ def testSetWebViewFallbackLogic_True_success(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.NOUGAT):
+ with self.assertCall(self.call.adb.Shell(
+ 'cmd webviewupdate disable-redundant-packages'), 'Success'):
+ self.device.SetWebViewFallbackLogic(True)
+
+ def testSetWebViewFallbackLogic_failure(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.NOUGAT):
+ with self.assertCall(self.call.adb.Shell(
+ 'cmd webviewupdate enable-redundant-packages'), 'Oops!'):
+ with self.assertRaises(device_errors.CommandFailedError):
+ self.device.SetWebViewFallbackLogic(False)
+
+ def testSetWebViewFallbackLogic_beforeNougat(self):
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=version_codes.MARSHMALLOW):
+ with self.assertCalls():
+ self.device.SetWebViewFallbackLogic(False)
+
+ def testSetWebViewFallbackLogic_afterPie(self):
+ # TODO(ntfschr): replace this with the Q constant when the SDK is public and
+ # the codename is finalized.
+ q_version_code = version_codes.PIE + 1
+ with self.patch_call(self.call.device.build_version_sdk,
+ return_value=q_version_code):
+ with self.assertCalls():
+ self.device.SetWebViewFallbackLogic(False)
class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest):
@@ -2745,9 +2967,7 @@ class DeviceUtilsTakeScreenshotTest(DeviceUtilsTest):
(mock.call.devil.android.device_temp_file.DeviceTempFile(
self.adb, suffix='.png'),
MockTempFile('/tmp/path/temp-123.png')),
- (self.call.adb.Shell(
- '/system/bin/screencap -p /tmp/path/temp-123.png',
- ensure_logs_on_timeout=False),
+ (self.call.adb.Shell('/system/bin/screencap -p /tmp/path/temp-123.png'),
''),
self.call.device.PullFile('/tmp/path/temp-123.png',
'/test/host/screenshot.png')):
@@ -2814,7 +3034,7 @@ class DeviceUtilsClientCache(DeviceUtilsTest):
self.assertEqual(self.device._cache['test'], 0)
self.assertEqual(client_cache_one, {'test': 1})
self.assertEqual(client_cache_two, {'test': 2})
- self.device._ClearCache()
+ self.device.ClearCache()
self.assertTrue('test' not in self.device._cache)
self.assertEqual(client_cache_one, {})
self.assertEqual(client_cache_two, {})
@@ -2825,7 +3045,7 @@ class DeviceUtilsClientCache(DeviceUtilsTest):
client_cache_two = self.device.GetClientCache('ClientOne')
self.assertEqual(client_cache_one, {'test': 1})
self.assertEqual(client_cache_two, {'test': 1})
- self.device._ClearCache()
+ self.device.ClearCache()
self.assertEqual(client_cache_one, {})
self.assertEqual(client_cache_two, {})
@@ -2838,9 +3058,9 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI),
+ abis.ARM),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI)):
+ abis.ARM)):
blacklist = mock.NonCallableMock(**{'Read.return_value': []})
devices = device_utils.DeviceUtils.HealthyDevices(blacklist)
for serial, device in zip(test_serials, devices):
@@ -2853,7 +3073,7 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI)):
+ abis.ARM)):
blacklist = mock.NonCallableMock(
**{'Read.return_value': ['fedcba9876543210']})
devices = device_utils.DeviceUtils.HealthyDevices(blacklist)
@@ -2867,9 +3087,9 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI),
+ abis.ARM),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI),
+ abis.ARM),
(mock.call.devil.android.device_errors.MultipleDevicesError(mock.ANY),
_MockMultipleDevicesError())):
with self.assertRaises(_MockMultipleDevicesError):
@@ -2881,7 +3101,7 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI)):
+ abis.ARM)):
devices = device_utils.DeviceUtils.HealthyDevices(device_arg=None)
self.assertEquals(1, len(devices))
@@ -2913,9 +3133,9 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI),
+ abis.ARM),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI)):
+ abis.ARM)):
devices = device_utils.DeviceUtils.HealthyDevices(device_arg=())
self.assertEquals(2, len(devices))
@@ -2952,6 +3172,30 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
self.assertEquals(mock_sleep.call_args_list, [
mock.call(2), mock.call(4), mock.call(8), mock.call(16)])
+ @mock.patch('time.sleep')
+ @mock.patch('devil.android.device_utils.RestartServer')
+ def testHealthyDevices_EmptyListDeviceArg_no_attached_with_resets(
+ self, mock_restart, mock_sleep):
+ # The reset_usb import fails on windows. Mock the full import here so it can
+ # succeed like it would on linux.
+ mock_reset_import = mock.MagicMock()
+ sys.modules['devil.utils.reset_usb'] = mock_reset_import
+ with self.assertCalls(
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), []),
+ (mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(), [])):
+ with self.assertRaises(device_errors.NoDevicesError):
+ with mock.patch.object(
+ mock_reset_import, 'reset_all_android_devices') as mock_reset:
+ device_utils.DeviceUtils.HealthyDevices(device_arg=[], retries=4,
+ enable_usb_resets=True)
+ self.assertEquals(mock_reset.call_count, 1)
+ self.assertEquals(mock_restart.call_count, 4)
+ self.assertEquals(mock_sleep.call_args_list, [
+ mock.call(2), mock.call(4), mock.call(8), mock.call(16)])
+
def testHealthyDevices_ListDeviceArg(self):
device_arg = ['0123456789abcdef', 'fedcba9876543210']
try:
@@ -2968,12 +3212,12 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI),
+ abis.ARM),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI)):
+ abis.ARM)):
with self.assertRaises(device_errors.NoDevicesError):
device_utils.DeviceUtils.HealthyDevices(device_arg=[], retries=0,
- abis=[ARM64_ABI])
+ abis=[abis.ARM_64])
def testHealthyDevices_abisArg_filter_on_abi(self):
test_serials = ['0123456789abcdef', 'fedcba9876543210']
@@ -2981,12 +3225,12 @@ class DeviceUtilsHealthyDevicesTest(mock_calls.TestCase):
(mock.call.devil.android.sdk.adb_wrapper.AdbWrapper.Devices(),
[_AdbWrapperMock(s) for s in test_serials]),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM64_ABI),
+ abis.ARM_64),
(mock.call.devil.android.device_utils.DeviceUtils.GetABI(),
- ARM32_ABI)):
+ abis.ARM)):
devices = device_utils.DeviceUtils.HealthyDevices(device_arg=[],
retries=0,
- abis=[ARM64_ABI])
+ abis=[abis.ARM_64])
self.assertEquals(1, len(devices))
@@ -3200,9 +3444,7 @@ class DeviceUtilsGetIMEITest(DeviceUtilsTest):
' Device ID = 123454321')
with self.assertCalls(
(self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
- (self.call.adb.Shell(
- 'dumpsys iphonesubinfo', ensure_logs_on_timeout=False),
- dumpsys_output)):
+ (self.call.adb.Shell('dumpsys iphonesubinfo'), dumpsys_output)):
self.assertEquals(self.device.GetIMEI(), '123454321')
def testSuccessfulServiceCall(self):
@@ -3214,25 +3456,20 @@ class DeviceUtilsGetIMEITest(DeviceUtilsTest):
"""
with self.assertCalls(
(self.call.device.GetProp('ro.build.version.sdk', cache=True), '24'),
- (self.call.adb.Shell(
- 'service call iphonesubinfo 1', ensure_logs_on_timeout=False),
- service_output)):
+ (self.call.adb.Shell('service call iphonesubinfo 1'), service_output)):
self.assertEquals(self.device.GetIMEI(), '765432101234567')
def testNoIMEI(self):
with self.assertCalls(
(self.call.device.GetProp('ro.build.version.sdk', cache=True), '19'),
- (self.call.adb.Shell(
- 'dumpsys iphonesubinfo', ensure_logs_on_timeout=False),
- 'no device id')):
+ (self.call.adb.Shell('dumpsys iphonesubinfo'), 'no device id')):
with self.assertRaises(device_errors.CommandFailedError):
self.device.GetIMEI()
def testAdbError(self):
with self.assertCalls(
(self.call.device.GetProp('ro.build.version.sdk', cache=True), '24'),
- (self.call.adb.Shell(
- 'service call iphonesubinfo 1', ensure_logs_on_timeout=False),
+ (self.call.adb.Shell('service call iphonesubinfo 1'),
self.ShellError())):
with self.assertRaises(device_errors.CommandFailedError):
self.device.GetIMEI()
@@ -3250,7 +3487,6 @@ class DeviceUtilsChangeOwner(DeviceUtilsTest):
class DeviceUtilsChangeSecurityContext(DeviceUtilsTest):
-
def testChangeSecurityContext(self):
with self.assertCalls(
(self.call.device.RunShellCommand(
@@ -3259,6 +3495,49 @@ class DeviceUtilsChangeSecurityContext(DeviceUtilsTest):
self.device.ChangeSecurityContext('u:object_r:system_data_file:s0',
['/path', '/path2'])
+
+class DeviceUtilsLocale(DeviceUtilsTest):
+
+ def testLocaleLegacy(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('persist.sys.locale', cache=False), ''),
+ (self.call.device.GetProp('persist.sys.language', cache=False), 'en'),
+ (self.call.device.GetProp('persist.sys.country', cache=False), 'US')):
+ self.assertEquals(self.device.GetLocale(), ('en', 'US'))
+
+ def testLocale(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('persist.sys.locale', cache=False), 'en-US'),
+ (self.call.device.GetProp('persist.sys.locale', cache=False),
+ 'en-US-sw')):
+ self.assertEquals(self.device.GetLocale(), ('en', 'US'))
+ self.assertEquals(self.device.GetLocale(), ('en', 'US-sw'))
+
+ def testBadLocale(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('persist.sys.locale', cache=False), 'en')):
+ self.assertEquals(self.device.GetLocale(), ('', ''))
+
+
+ def testLanguageAndCountryLegacy(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('persist.sys.locale', cache=False), ''),
+ (self.call.device.GetProp('persist.sys.language', cache=False), 'en'),
+ (self.call.device.GetProp('persist.sys.country', cache=False), 'US'),
+ (self.call.device.GetProp('persist.sys.locale', cache=False), ''),
+ (self.call.device.GetProp('persist.sys.language', cache=False), 'en'),
+ (self.call.device.GetProp('persist.sys.country', cache=False), 'US')):
+ self.assertEquals(self.device.GetLanguage(), 'en')
+ self.assertEquals(self.device.GetCountry(), 'US')
+
+ def testLanguageAndCountry(self):
+ with self.assertCalls(
+ (self.call.device.GetProp('persist.sys.locale', cache=False), 'en-US'),
+ (self.call.device.GetProp('persist.sys.locale', cache=False), 'en-US')):
+ self.assertEquals(self.device.GetLanguage(), 'en')
+ self.assertEquals(self.device.GetCountry(), 'US')
+
+
if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
unittest.main(verbosity=2)
diff --git a/catapult/devil/devil/android/fastboot_utils.py b/catapult/devil/devil/android/fastboot_utils.py
index 3bd3ee8b..3621d7fb 100644
--- a/catapult/devil/devil/android/fastboot_utils.py
+++ b/catapult/devil/devil/android/fastboot_utils.py
@@ -108,7 +108,7 @@ class FastbootUtils(object):
This waits for the device serial to show up in fastboot devices output.
"""
def fastboot_mode():
- return self._serial in self.fastboot.Devices()
+ return any(self._serial == str(d) for d in self.fastboot.Devices())
timeout_retry.WaitFor(fastboot_mode, wait_period=self._FASTBOOT_WAIT_TIME)
diff --git a/catapult/devil/devil/android/flag_changer.py b/catapult/devil/devil/android/flag_changer.py
index c96dbadc..110cf827 100644
--- a/catapult/devil/devil/android/flag_changer.py
+++ b/catapult/devil/devil/android/flag_changer.py
@@ -74,6 +74,8 @@ class FlagChanger(object):
if use_legacy_path:
cmdline_path, alternate_cmdline_path = (
alternate_cmdline_path, cmdline_path)
+ if not self._device.HasRoot():
+ raise ValueError('use_legacy_path requires a rooted device')
self._cmdline_path = cmdline_path
if self._device.PathExists(alternate_cmdline_path):
@@ -103,7 +105,7 @@ class FlagChanger(object):
self._state_stack[-1] = set(flags)
return flags
- def ReplaceFlags(self, flags):
+ def ReplaceFlags(self, flags, log_flags=True):
"""Replaces the flags in the command line with the ones provided.
Saves the current flags state on the stack, so a call to Restore will
change the state back to the one preceeding the call to ReplaceFlags.
@@ -119,7 +121,7 @@ class FlagChanger(object):
new_flags = set(flags)
self._state_stack.append(new_flags)
self._SetPermissive()
- return self._UpdateCommandLineFile()
+ return self._UpdateCommandLineFile(log_flags=log_flags)
def AddFlags(self, flags):
"""Appends flags to the command line if they aren't already there.
@@ -179,10 +181,14 @@ class FlagChanger(object):
"""Set SELinux to permissive, if needed.
On Android N and above this is needed in order to allow Chrome to read the
- command line file.
+ legacy command line file.
TODO(crbug.com/699082): Remove when a better solution exists.
"""
+ # TODO(crbug.com/948578): figure out the exact scenarios where the lowered
+ # permissions are needed, and document them in the code.
+ if not self._device.HasRoot():
+ return
if (self._device.build_version_sdk >= version_codes.NOUGAT and
self._device.GetEnforce()):
self._device.SetEnforce(enabled=False)
@@ -209,7 +215,7 @@ class FlagChanger(object):
self._ResetEnforce()
return self._UpdateCommandLineFile()
- def _UpdateCommandLineFile(self):
+ def _UpdateCommandLineFile(self, log_flags=True):
"""Writes out the command line to the file, or removes it if empty.
Returns:
@@ -221,9 +227,11 @@ class FlagChanger(object):
else:
self._device.RemovePath(self._cmdline_path, force=True, as_root=True)
- current_flags = self.GetCurrentFlags()
- logger.info('Flags now set on the device: %s', current_flags)
- return current_flags
+ flags = self.GetCurrentFlags()
+ logging.info('Flags now written on the device to %s', self._cmdline_path)
+ if log_flags:
+ logging.info('Flags: %s', flags)
+ return flags
def _ParseFlags(line):
diff --git a/catapult/devil/devil/android/logcat_monitor.py b/catapult/devil/devil/android/logcat_monitor.py
index 249320b7..b5f796b7 100644
--- a/catapult/devil/devil/android/logcat_monitor.py
+++ b/catapult/devil/devil/android/logcat_monitor.py
@@ -31,7 +31,7 @@ class LogcatMonitor(object):
r'(?P<log_level>%s) +(?P<component>%s) *: +(?P<message>%s)$')
def __init__(self, adb, clear=True, filter_specs=None, output_file=None,
- transform_func=None):
+ transform_func=None, check_error=True):
"""Create a LogcatMonitor instance.
Args:
@@ -41,11 +41,14 @@ class LogcatMonitor(object):
output_file: File path to save recorded logcat.
transform_func: An optional unary callable that takes and returns
a list of lines, possibly transforming them in the process.
+ check_error: Check for and raise an exception on nonzero exit codes
+ from the underlying logcat command.
"""
if isinstance(adb, adb_wrapper.AdbWrapper):
self._adb = adb
else:
raise ValueError('Unsupported type passed for argument "device"')
+ self._check_error = check_error
self._clear = clear
self._filter_specs = filter_specs
self._output_file = output_file
@@ -168,9 +171,11 @@ class LogcatMonitor(object):
def record_to_file():
# Write the log with line buffering so the consumer sees each individual
# line.
- for data in self._adb.Logcat(filter_specs=self._filter_specs,
- logcat_format='threadtime',
- iter_timeout=self._RECORD_ITER_TIMEOUT):
+ for data in self._adb.Logcat(
+ filter_specs=self._filter_specs,
+ logcat_format='threadtime',
+ iter_timeout=self._RECORD_ITER_TIMEOUT,
+ check_error=self._check_error):
if self._stop_recording_event.isSet():
return
diff --git a/catapult/devil/devil/android/md5sum.py b/catapult/devil/devil/android/md5sum.py
index 6dece9e8..f5b6f3cf 100644
--- a/catapult/devil/devil/android/md5sum.py
+++ b/catapult/devil/devil/android/md5sum.py
@@ -89,7 +89,8 @@ def CalculateDeviceMd5Sums(paths, device):
# Note: ":" is equivalent to "true".
md5sum_script += ';:'
try:
- out = device.RunShellCommand(md5sum_script, shell=True, check_return=True)
+ out = device.RunShellCommand(
+ md5sum_script, shell=True, check_return=True, large_output=True)
except device_errors.AdbShellCommandFailedError as e:
# Push the binary only if it is found to not exist
# (faster than checking up-front).
@@ -106,7 +107,8 @@ def CalculateDeviceMd5Sums(paths, device):
device.RunShellCommand(mkdir_cmd, shell=True, check_return=True)
device.adb.Push(md5sum_dist_bin_path, MD5SUM_DEVICE_BIN_PATH)
- out = device.RunShellCommand(md5sum_script, shell=True, check_return=True)
+ out = device.RunShellCommand(
+ md5sum_script, shell=True, check_return=True, large_output=True)
else:
raise
diff --git a/catapult/devil/devil/android/ndk/__init__.py b/catapult/devil/devil/android/ndk/__init__.py
new file mode 100644
index 00000000..edd8dbc1
--- /dev/null
+++ b/catapult/devil/devil/android/ndk/__init__.py
@@ -0,0 +1,6 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This package is intended for modules that are very tightly coupled to
+# tools or APIs from the Android NDK.
diff --git a/catapult/devil/devil/android/ndk/abis.py b/catapult/devil/devil/android/ndk/abis.py
new file mode 100644
index 00000000..dd32f7cb
--- /dev/null
+++ b/catapult/devil/devil/android/ndk/abis.py
@@ -0,0 +1,16 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Android NDK ABIs.
+
+https://developer.android.com/ndk/guides/abis
+
+These constants can be compared against the value of
+devil.android.DeviceUtils.product_cpu_abi.
+"""
+
+ARM = 'armeabi-v7a'
+ARM_64 = 'arm64-v8a'
+X86 = 'x86'
+X86_64 = 'x86_64'
diff --git a/catapult/devil/devil/android/perf/perf_control.py b/catapult/devil/devil/android/perf/perf_control.py
index 2aa3b2f2..1226be80 100644
--- a/catapult/devil/devil/android/perf/perf_control.py
+++ b/catapult/devil/devil/android/perf/perf_control.py
@@ -28,12 +28,40 @@ _PERFORMANCE_MODE_DEFINITIONS = {
'AFTKMST12': {
'default_mode_governor': 'interactive',
},
+ # Pixel 3
+ 'blueline': {
+ 'high_perf_mode': {
+ 'bring_cpu_cores_online': True,
+ # The SoC is Arm big.LITTLE. The cores 0..3 are LITTLE, the 4..7 are big.
+ 'cpu_max_freq': {'0..3': 1228800, '4..7': 1536000},
+ 'gpu_max_freq': 520000000,
+ },
+ 'default_mode': {
+ 'cpu_max_freq': {'0..3': 1766400, '4..7': 2649600},
+ 'gpu_max_freq': 710000000,
+ },
+ 'default_mode_governor': 'schedutil',
+ },
'GT-I9300': {
'default_mode_governor': 'pegasusq',
},
'Galaxy Nexus': {
'default_mode_governor': 'interactive',
},
+ # Pixel
+ 'msm8996': {
+ 'high_perf_mode': {
+ 'bring_cpu_cores_online': True,
+ 'cpu_max_freq': 1209600,
+ 'gpu_max_freq': 315000000,
+ },
+ 'default_mode': {
+ # The SoC is Arm big.LITTLE. The cores 0..1 are LITTLE, the 2..3 are big.
+ 'cpu_max_freq': {'0..1': 1593600, '2..3': 2150400},
+ 'gpu_max_freq': 624000000,
+ },
+ 'default_mode_governor': 'sched',
+ },
'Nexus 7': {
'default_mode_governor': 'interactive',
},
@@ -78,6 +106,10 @@ _PERFORMANCE_MODE_DEFINITIONS = {
},
}
+def _GetPerfModeDefinitions(product_model):
+ if product_model.startswith('AOSP on '):
+ product_model = product_model.replace('AOSP on ', '')
+ return _PERFORMANCE_MODE_DEFINITIONS.get(product_model)
def _NoisyWarning(message):
message += ' Results may be NOISY!!'
@@ -148,8 +180,7 @@ class PerfControl(object):
except device_errors.CommandFailedError:
_NoisyWarning('Need root for performance mode.')
return
- mode_definitions = _PERFORMANCE_MODE_DEFINITIONS.get(
- self._device.product_model)
+ mode_definitions = _GetPerfModeDefinitions(self._device.product_model)
if not mode_definitions:
self.SetScalingGovernor('performance')
return
@@ -170,8 +201,7 @@ class PerfControl(object):
"""Sets the performance mode for the device to its default mode."""
if not self._device.HasRoot():
return
- mode_definitions = _PERFORMANCE_MODE_DEFINITIONS.get(
- self._device.product_model)
+ mode_definitions = _GetPerfModeDefinitions(self._device.product_model)
if not mode_definitions:
self.SetScalingGovernor('ondemand')
else:
diff --git a/catapult/devil/devil/android/perf/surface_stats_collector.py b/catapult/devil/devil/android/perf/surface_stats_collector.py
index ea46a398..f1140c12 100644
--- a/catapult/devil/devil/android/perf/surface_stats_collector.py
+++ b/catapult/devil/devil/android/perf/surface_stats_collector.py
@@ -2,7 +2,9 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import logging
import Queue
+import re
import threading
@@ -135,61 +137,73 @@ class SurfaceStatsCollector(object):
The return value may be (None, None) if there was no data collected (for
example, if the app was closed before the collector thread has finished).
"""
- # adb shell dumpsys SurfaceFlinger --latency <window name>
- # prints some information about the last 128 frames displayed in
- # that window.
- # The data returned looks like this:
- # 16954612
- # 7657467895508 7657482691352 7657493499756
- # 7657484466553 7657499645964 7657511077881
- # 7657500793457 7657516600576 7657527404785
- # (...)
- #
- # The first line is the refresh period (here 16.95 ms), it is followed
- # by 128 lines w/ 3 timestamps in nanosecond each:
- # A) when the app started to draw
- # B) the vsync immediately preceding SF submitting the frame to the h/w
- # C) timestamp immediately after SF submitted that frame to the h/w
- #
- # The difference between the 1st and 3rd timestamp is the frame-latency.
- # An interesting data is when the frame latency crosses a refresh period
- # boundary, this can be calculated this way:
- #
- # ceil((C - A) / refresh-period)
- #
- # (each time the number above changes, we have a "jank").
- # If this happens a lot during an animation, the animation appears
- # janky, even if it runs at 60 fps in average.
window_name = self._GetSurfaceViewWindowName()
command = ['dumpsys', 'SurfaceFlinger', '--latency']
# Even if we don't find the window name, run the command to get the refresh
# period.
if window_name:
command.append(window_name)
- results = self._device.RunShellCommand(command, check_return=True)
- if not len(results):
- return (None, None)
-
- timestamps = []
- nanoseconds_per_millisecond = 1e6
- refresh_period = long(results[0]) / nanoseconds_per_millisecond
- if not window_name:
- return (refresh_period, timestamps)
-
- # If a fence associated with a frame is still pending when we query the
- # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
- # Since we only care about completed frames, we will ignore any timestamps
- # with this value.
- pending_fence_timestamp = (1 << 63) - 1
-
- for line in results[1:]:
- fields = line.split()
- if len(fields) != 3:
- continue
- timestamp = long(fields[1])
- if timestamp == pending_fence_timestamp:
- continue
- timestamp /= nanoseconds_per_millisecond
- timestamps.append(timestamp)
-
- return (refresh_period, timestamps)
+ output = self._device.RunShellCommand(command, check_return=True)
+ return ParseFrameData(output, parse_timestamps=bool(window_name))
+
+
+def ParseFrameData(lines, parse_timestamps):
+ # adb shell dumpsys SurfaceFlinger --latency <window name>
+ # prints some information about the last 128 frames displayed in
+ # that window.
+ # The data returned looks like this:
+ # 16954612
+ # 7657467895508 7657482691352 7657493499756
+ # 7657484466553 7657499645964 7657511077881
+ # 7657500793457 7657516600576 7657527404785
+ # (...)
+ #
+ # The first line is the refresh period (here 16.95 ms), it is followed
+ # by 128 lines w/ 3 timestamps in nanosecond each:
+ # A) when the app started to draw
+ # B) the vsync immediately preceding SF submitting the frame to the h/w
+ # C) timestamp immediately after SF submitted that frame to the h/w
+ #
+ # The difference between the 1st and 3rd timestamp is the frame-latency.
+ # An interesting data is when the frame latency crosses a refresh period
+ # boundary, this can be calculated this way:
+ #
+ # ceil((C - A) / refresh-period)
+ #
+ # (each time the number above changes, we have a "jank").
+ # If this happens a lot during an animation, the animation appears
+ # janky, even if it runs at 60 fps in average.
+ results = []
+ for line in lines:
+ # Skip over lines with anything other than digits and whitespace.
+ if re.search(r'[^\d\s]', line):
+ logging.warning('unexpected output: %s', line)
+ else:
+ results.append(line)
+ if not results:
+ return None, None
+
+ timestamps = []
+ nanoseconds_per_millisecond = 1e6
+ refresh_period = long(results[0]) / nanoseconds_per_millisecond
+ if not parse_timestamps:
+ return refresh_period, timestamps
+
+ # If a fence associated with a frame is still pending when we query the
+ # latency data, SurfaceFlinger gives the frame a timestamp of INT64_MAX.
+ # Since we only care about completed frames, we will ignore any timestamps
+ # with this value.
+ pending_fence_timestamp = (1 << 63) - 1
+
+ for line in results[1:]:
+ fields = line.split()
+ if len(fields) != 3:
+ logging.warning('Unexpected line: %s', line)
+ continue
+ timestamp = long(fields[1])
+ if timestamp == pending_fence_timestamp:
+ continue
+ timestamp /= nanoseconds_per_millisecond
+ timestamps.append(timestamp)
+
+ return refresh_period, timestamps
diff --git a/catapult/devil/devil/android/perf/surface_stats_collector_test.py b/catapult/devil/devil/android/perf/surface_stats_collector_test.py
new file mode 100644
index 00000000..13b345ce
--- /dev/null
+++ b/catapult/devil/devil/android/perf/surface_stats_collector_test.py
@@ -0,0 +1,40 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+from devil.android.perf import surface_stats_collector
+
+
+class SurfaceStatsCollectorTests(unittest.TestCase):
+ def testParseFrameData_simple(self):
+ actual = surface_stats_collector.ParseFrameData([
+ '16954612',
+ '7657467895508 7657482691352 7657493499756',
+ '7657484466553 7657499645964 7657511077881',
+ '7657500793457 7657516600576 7657527404785',
+ ], parse_timestamps=True)
+ self.assertEqual(
+ actual, (16.954612, [7657482.691352, 7657499.645964, 7657516.600576]))
+
+ def testParseFrameData_withoutTimestamps(self):
+ actual = surface_stats_collector.ParseFrameData([
+ '16954612',
+ '7657467895508 7657482691352 7657493499756',
+ '7657484466553 7657499645964 7657511077881',
+ '7657500793457 7657516600576 7657527404785',
+ ], parse_timestamps=False)
+ self.assertEqual(
+ actual, (16.954612, []))
+
+ def testParseFrameData_withWarning(self):
+ actual = surface_stats_collector.ParseFrameData([
+ 'SurfaceFlinger appears to be unresponsive, dumping anyways',
+ '16954612',
+ '7657467895508 7657482691352 7657493499756',
+ '7657484466553 7657499645964 7657511077881',
+ '7657500793457 7657516600576 7657527404785',
+ ], parse_timestamps=True)
+ self.assertEqual(
+ actual, (16.954612, [7657482.691352, 7657499.645964, 7657516.600576]))
diff --git a/catapult/devil/devil/android/ports.py b/catapult/devil/devil/android/ports.py
index 1d4e5f21..4547a627 100644
--- a/catapult/devil/devil/android/ports.py
+++ b/catapult/devil/devil/android/ports.py
@@ -116,7 +116,7 @@ def IsDevicePortUsed(device, device_port, state=''):
"""
base_urls = ('127.0.0.1:%d' % device_port, 'localhost:%d' % device_port)
netstat_results = device.RunShellCommand(
- ['netstat', '-a'], check_return=True, large_output=True)
+ ['netstat', '-an'], check_return=True, large_output=True)
for single_connect in netstat_results:
# Column 3 is the local address which we want to check with.
connect_results = single_connect.split()
diff --git a/catapult/devil/devil/android/sdk/adb_wrapper.py b/catapult/devil/devil/android/sdk/adb_wrapper.py
index 2fbe9638..13c0f520 100644
--- a/catapult/devil/devil/android/sdk/adb_wrapper.py
+++ b/catapult/devil/devil/android/sdk/adb_wrapper.py
@@ -34,6 +34,7 @@ logger = logging.getLogger(__name__)
ADB_KEYS_FILE = '/data/misc/adb/adb_keys'
+ADB_HOST_KEYS_DIR = os.path.join(os.path.expanduser('~'), '.android')
DEFAULT_TIMEOUT = 30
DEFAULT_RETRIES = 2
@@ -262,15 +263,22 @@ class AdbWrapper(object):
@classmethod
@decorators.WithTimeoutAndConditionalRetries(_ShouldRetryAdbCmd)
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None,
- check_error=True, cpu_affinity=None,
- ensure_logs_on_timeout=False):
- timeout = timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime()
- if ensure_logs_on_timeout:
- timeout = 0.95 * timeout
+ check_error=True, cpu_affinity=None, additional_env=None):
+ if timeout:
+ remaining = timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime()
+ if remaining:
+ # Use a slightly smaller timeout than remaining time to ensure that we
+ # have time to collect output from the command.
+ timeout = 0.95 * remaining
+ else:
+ timeout = None
+ env = cls._ADB_ENV.copy()
+ if additional_env:
+ env.update(additional_env)
try:
status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity),
- timeout, env=cls._ADB_ENV)
+ timeout, env=env)
except OSError as e:
if e.errno in (errno.ENOENT, errno.ENOEXEC):
raise device_errors.NoAdbError(msg=str(e))
@@ -294,9 +302,7 @@ class AdbWrapper(object):
return output
# pylint: enable=unused-argument
- def _RunDeviceAdbCmd(
- self, args, timeout, retries, check_error=True,
- ensure_logs_on_timeout=False):
+ def _RunDeviceAdbCmd(self, args, timeout, retries, check_error=True):
"""Runs an adb command on the device associated with this object.
Args:
@@ -304,23 +310,27 @@ class AdbWrapper(object):
timeout: Timeout in seconds.
retries: Number of retries.
check_error: Check that the command doesn't return an error message. This
- does NOT check the exit status of shell commands.
+ does check the error status of adb but DOES NOT check the exit status
+ of shell commands.
Returns:
The output of the command.
"""
return self._RunAdbCmd(args, timeout=timeout, retries=retries,
device_serial=self._device_serial,
- check_error=check_error,
- ensure_logs_on_timeout=ensure_logs_on_timeout)
+ check_error=check_error)
- def _IterRunDeviceAdbCmd(self, args, iter_timeout, timeout):
+ def _IterRunDeviceAdbCmd(self, args, iter_timeout, timeout,
+ check_error=True):
"""Runs an adb command and returns an iterator over its output lines.
Args:
args: A list of arguments to adb.
iter_timeout: Timeout for each iteration in seconds.
timeout: Timeout for the entire command in seconds.
+ check_error: Check that the command succeeded. This does check the
+ error status of the adb command but DOES NOT check the exit status
+ of shell commands.
Yields:
The output of the command line by line.
@@ -329,7 +339,8 @@ class AdbWrapper(object):
self._BuildAdbCmd(args, self._device_serial),
iter_timeout=iter_timeout,
timeout=timeout,
- env=self._ADB_ENV)
+ env=self._ADB_ENV,
+ check_status=check_error)
def __eq__(self, other):
"""Consider instances equal if they refer to the same device.
@@ -367,10 +378,21 @@ class AdbWrapper(object):
cls._RunAdbCmd(['kill-server'], timeout=timeout, retries=retries)
@classmethod
- def StartServer(cls, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
+ def StartServer(cls, keys=None, timeout=DEFAULT_TIMEOUT,
+ retries=DEFAULT_RETRIES):
+ """Starts the ADB server.
+
+ Args:
+ keys: (optional) List of local ADB keys to use to auth with devices.
+ timeout: (optional) Timeout per try in seconds.
+ retries: (optional) Number of retries to attempt.
+ """
+ additional_env = {}
+ if keys:
+ additional_env['ADB_VENDOR_KEYS'] = ':'.join(keys)
# CPU affinity is used to reduce adb instability http://crbug.com/268450
cls._RunAdbCmd(['start-server'], timeout=timeout, retries=retries,
- cpu_affinity=0)
+ cpu_affinity=0, additional_env=additional_env)
@classmethod
def GetDevices(cls, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
@@ -507,17 +529,14 @@ class AdbWrapper(object):
return cmd_helper.StartCmd(
self._BuildAdbCmd(['shell'] + cmd, self._device_serial))
- def Shell(self, command, expect_status=0, ensure_logs_on_timeout=False,
- timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
+ def Shell(self, command, expect_status=0, timeout=DEFAULT_TIMEOUT,
+ retries=DEFAULT_RETRIES):
"""Runs a shell command on the device.
Args:
command: A string with the shell command to run.
expect_status: (optional) Check that the command's exit status matches
this value. Default is 0. If set to None the test is skipped.
- ensure_logs_on_timeout: If True, will use a timeout that is 5% smaller
- than the remaining time on the thread watchdog for the internal adb
- command, which allows to retrive logs on timeout.
timeout: (optional) Timeout per try in seconds.
retries: (optional) Number of retries to attempt.
@@ -532,9 +551,7 @@ class AdbWrapper(object):
args = ['shell', command]
else:
args = ['shell', '( %s );echo %%$?' % command.rstrip()]
- output = self._RunDeviceAdbCmd(
- args, timeout, retries, check_error=False,
- ensure_logs_on_timeout=ensure_logs_on_timeout)
+ output = self._RunDeviceAdbCmd(args, timeout, retries, check_error=False)
if expect_status is not None:
output_end = output.rfind('%')
if output_end < 0:
@@ -607,7 +624,7 @@ class AdbWrapper(object):
def Logcat(self, clear=False, dump=False, filter_specs=None,
logcat_format=None, ring_buffer=None, iter_timeout=None,
- timeout=None, retries=DEFAULT_RETRIES):
+ check_error=True, timeout=None, retries=DEFAULT_RETRIES):
"""Get an iterable over the logcat output.
Args:
@@ -623,6 +640,7 @@ class AdbWrapper(object):
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.
+ check_error: Whether to check the exit status of the logcat command.
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
@@ -648,10 +666,13 @@ class AdbWrapper(object):
cmd.extend(filter_specs)
if use_iter:
- return self._IterRunDeviceAdbCmd(cmd, iter_timeout, timeout)
+ return self._IterRunDeviceAdbCmd(
+ cmd, iter_timeout, timeout, check_error=check_error)
else:
timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
- return self._RunDeviceAdbCmd(cmd, timeout, retries).splitlines()
+ output = self._RunDeviceAdbCmd(
+ cmd, timeout, retries, check_error=check_error)
+ return output.splitlines()
def Forward(self, local, remote, allow_rebind=False,
timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
@@ -947,18 +968,28 @@ class AdbWrapper(object):
return self._RunDeviceAdbCmd(['emu'] + cmd, timeout, retries)
def DisableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
- """Disable Marshmallow's Verity security feature"""
+ """Disable Marshmallow's Verity security feature.
+
+ Returns:
+ The output of the disable-verity command as a string.
+ """
output = self._RunDeviceAdbCmd(['disable-verity'], timeout, retries)
if output and not _VERITY_DISABLE_RE.search(output):
raise device_errors.AdbCommandFailedError(
['disable-verity'], output, device_serial=self._device_serial)
+ return output
def EnableVerity(self, timeout=DEFAULT_TIMEOUT, retries=DEFAULT_RETRIES):
- """Enable Marshmallow's Verity security feature"""
+ """Enable Marshmallow's Verity security feature.
+
+ Returns:
+ The output of the enable-verity command as a string.
+ """
output = self._RunDeviceAdbCmd(['enable-verity'], timeout, retries)
if output and not _VERITY_ENABLE_RE.search(output):
raise device_errors.AdbCommandFailedError(
['enable-verity'], output, device_serial=self._device_serial)
+ return output
@property
def is_emulator(self):
diff --git a/catapult/devil/devil/android/tools/device_recovery.py b/catapult/devil/devil/android/tools/device_recovery.py
index e8b9ba3f..8050e6fe 100755
--- a/catapult/devil/devil/android/tools/device_recovery.py
+++ b/catapult/devil/devil/android/tools/device_recovery.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env vpython
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
@@ -6,6 +6,7 @@
"""A script to recover devices in a known bad state."""
import argparse
+import glob
import logging
import os
import signal
@@ -30,14 +31,18 @@ from devil.utils import reset_usb # pylint: disable=unused-import
logger = logging.getLogger(__name__)
+from py_utils import modules_util
+
+
+# Script depends on features from psutil version 2.0 or higher.
+modules_util.RequireVersion(psutil, '2.0')
+
def KillAllAdb():
def get_all_adb():
for p in psutil.process_iter():
try:
- # Note: p.as_dict is compatible with both older (v1 and under) as well
- # as newer (v2 and over) versions of psutil.
- # See: http://grodola.blogspot.com/2014/01/psutil-20-porting.html
+ # Retrieve all required process infos at once.
pinfo = p.as_dict(attrs=['pid', 'name', 'cmdline'])
if pinfo['name'] == 'adb':
pinfo['cmdline'] = ' '.join(pinfo['cmdline'])
@@ -60,12 +65,58 @@ def KillAllAdb():
pass
+def TryAuth(device):
+ """Uses anything in ~/.android/ that looks like a key to auth with the device.
+
+ Args:
+ device: The DeviceUtils device to attempt to auth.
+
+ Returns:
+ True if device successfully authed.
+ """
+ possible_keys = glob.glob(os.path.join(adb_wrapper.ADB_HOST_KEYS_DIR, '*key'))
+ if len(possible_keys) <= 1:
+ logger.warning(
+ 'Only %d ADB keys available. Not forcing auth.', len(possible_keys))
+ return False
+
+ KillAllAdb()
+ adb_wrapper.AdbWrapper.StartServer(keys=possible_keys)
+ new_state = device.adb.GetState()
+ if new_state != 'device':
+ logger.error(
+ 'Auth failed. Device %s still stuck in %s.', str(device), new_state)
+ return False
+
+ # It worked! Now register the host's default ADB key on the device so we don't
+ # have to do all that again.
+ pub_key = os.path.join(adb_wrapper.ADB_HOST_KEYS_DIR, 'adbkey.pub')
+ if not os.path.exists(pub_key): # This really shouldn't happen.
+ logger.error('Default ADB key not available at %s.', pub_key)
+ return False
+
+ with open(pub_key) as f:
+ pub_key_contents = f.read()
+ try:
+ device.WriteFile(adb_wrapper.ADB_KEYS_FILE, pub_key_contents, as_root=True)
+ except (device_errors.CommandTimeoutError,
+ device_errors.CommandFailedError,
+ device_errors.DeviceUnreachableError):
+ logger.exception('Unable to write default ADB key to %s.', str(device))
+ return False
+ return True
+
+
def RecoverDevice(device, blacklist, should_reboot=lambda device: True):
if device_status.IsBlacklisted(device.adb.GetDeviceSerial(),
blacklist):
logger.debug('%s is blacklisted, skipping recovery.', str(device))
return
+ if device.adb.GetState() == 'unauthorized' and TryAuth(device):
+ logger.info('Successfully authed device %s!', str(device))
+ return
+
if should_reboot(device):
try:
device.WaitUntilFullyBooted(retries=0)
@@ -136,7 +187,7 @@ def RecoverDevices(devices, blacklist, enable_usb_reset=False):
should_restart_adb = should_restart_usb.union(set(
status['serial'] for status in statuses
if status['adb_status'] == 'unauthorized'))
- should_reboot_device = should_restart_adb.union(set(
+ should_reboot_device = should_restart_usb.union(set(
status['serial'] for status in statuses
if status['blacklisted']))
diff --git a/catapult/devil/devil/android/tools/script_common.py b/catapult/devil/devil/android/tools/script_common.py
index 150e63fb..897659bb 100644
--- a/catapult/devil/devil/android/tools/script_common.py
+++ b/catapult/devil/devil/android/tools/script_common.py
@@ -11,13 +11,38 @@ from devil.android import device_utils
def AddEnvironmentArguments(parser):
- """Adds environment-specific arguments to the provided parser."""
+ """Adds environment-specific arguments to the provided parser.
+
+ After adding these arguments, you must pass the user-specified values when
+ initializing devil. See the InitializeEnvironment() to determine how to do so.
+
+ Args:
+ parser: an instance of argparse.ArgumentParser
+ """
parser.add_argument(
'--adb-path', type=os.path.realpath,
help='Path to the adb binary')
def InitializeEnvironment(args):
+ """Initializes devil based on the args added by AddEnvironmentArguments().
+
+ This initializes devil, and configures it to use the adb binary specified by
+ the '--adb-path' flag (if provided by the user, otherwise this defaults to
+ devil's copy of adb). Although this is one possible way to initialize devil,
+ you should check if your project has prefered ways to initialize devil (ex.
+ the chromium project uses devil_chromium.Initialize() to have different
+ defaults for dependencies).
+
+ This method requires having previously called AddEnvironmentArguments() on the
+ relevant argparse.ArgumentParser.
+
+ Note: you should only initialize devil once, and subsequent calls to any
+ method wrapping devil_env.config.Initialize() will have no effect.
+
+ Args:
+ args: the parsed args returned by an argparse.ArgumentParser
+ """
devil_dynamic_config = devil_env.EmptyConfig()
if args.adb_path:
devil_dynamic_config['dependencies'].update(
@@ -28,7 +53,11 @@ def InitializeEnvironment(args):
def AddDeviceArguments(parser):
- """Adds device and blacklist arguments to the provided parser."""
+ """Adds device and blacklist arguments to the provided parser.
+
+ Args:
+ parser: an instance of argparse.ArgumentParser
+ """
parser.add_argument(
'-d', '--device', dest='devices', action='append',
help='Serial number of the Android device to use. (default: use all)')
diff --git a/catapult/devil/devil/android/tools/system_app.py b/catapult/devil/devil/android/tools/system_app.py
index 4fe35e57..8629ae68 100755
--- a/catapult/devil/devil/android/tools/system_app.py
+++ b/catapult/devil/devil/android/tools/system_app.py
@@ -10,6 +10,7 @@ import contextlib
import logging
import os
import posixpath
+import re
import sys
@@ -31,6 +32,19 @@ from devil.utils import run_tests_helper
logger = logging.getLogger(__name__)
+# Some system apps aren't actually installed in the /system/ directory, so
+# special case them here with the correct install location.
+SPECIAL_SYSTEM_APP_LOCATIONS = {
+ # This also gets installed in /data/app when not a system app, so this script
+ # will remove either version. This doesn't appear to cause any issues, but
+ # will cause a few unnecessary reboots if this is the only package getting
+ # removed and it's already not a system app.
+ 'com.google.ar.core': '/data/app/',
+}
+
+# Gets app path and package name pm list packages -f output.
+_PM_LIST_PACKAGE_PATH_RE = re.compile(r'^\s*package:(\S+)=(\S+)\s*$')
+
def RemoveSystemApps(device, package_names):
"""Removes the given system apps.
@@ -46,7 +60,8 @@ def RemoveSystemApps(device, package_names):
@contextlib.contextmanager
-def ReplaceSystemApp(device, package_name, replacement_apk):
+def ReplaceSystemApp(device, package_name, replacement_apk,
+ install_timeout=None):
"""A context manager that replaces the given system app while in scope.
Args:
@@ -57,7 +72,7 @@ def ReplaceSystemApp(device, package_name, replacement_apk):
"""
storage_dir = device_temp_file.NamedDeviceTemporaryDirectory(device.adb)
relocate_app = _RelocateApp(device, package_name, storage_dir.name)
- install_app = _TemporarilyInstallApp(device, replacement_apk)
+ install_app = _TemporarilyInstallApp(device, replacement_apk, install_timeout)
with storage_dir, relocate_app, install_app:
yield
@@ -66,8 +81,36 @@ def _FindSystemPackagePaths(device, system_package_list):
"""Finds all system paths for the given packages."""
found_paths = []
for system_package in system_package_list:
- found_paths.extend(device.GetApplicationPaths(system_package))
- return [p for p in found_paths if p.startswith('/system/')]
+ paths = _GetApplicationPaths(device, system_package)
+ p = _GetSystemPath(system_package, paths)
+ if p:
+ found_paths.append(p)
+ return found_paths
+
+
+# Find all application paths, even those flagged as uninstalled, as these
+# would still block another package with the same name from installation
+# if they differ in signing keys.
+# TODO(aluo): Move this into device_utils.py
+def _GetApplicationPaths(device, package):
+ paths = []
+ lines = device.RunShellCommand(['pm', 'list', 'packages', '-f', '-u',
+ package], check_return=True)
+ for line in lines:
+ match = re.match(_PM_LIST_PACKAGE_PATH_RE, line)
+ if match:
+ path = match.group(1)
+ package_name = match.group(2)
+ if package_name == package:
+ paths.append(path)
+ return paths
+
+
+def _GetSystemPath(package, paths):
+ for p in paths:
+ if p.startswith(SPECIAL_SYSTEM_APP_LOCATIONS.get(package, '/system/')):
+ return p
+ return None
_ENABLE_MODIFICATION_PROP = 'devil.modify_sys_apps'
@@ -84,6 +127,12 @@ def EnableSystemAppModification(device):
yield
return
+ # All calls that could potentially need root should run with as_root=True, but
+ # it looks like some parts of Telemetry work as-is by implicitly assuming that
+ # root is already granted if it's necessary. The reboot can mess with this, so
+ # as a workaround, check whether we're starting with root already, and if so,
+ # restore the device to that state at the end.
+ should_restore_root = device.HasRoot()
device.EnableRoot()
if not device.HasRoot():
raise device_errors.CommandFailedError(
@@ -107,6 +156,8 @@ def EnableSystemAppModification(device):
device.SetProp(_ENABLE_MODIFICATION_PROP, '0')
device.Reboot()
device.WaitUntilFullyBooted()
+ if should_restore_root:
+ device.EnableRoot()
@contextlib.contextmanager
@@ -136,9 +187,13 @@ def _RelocateApp(device, package_name, relocate_to):
@contextlib.contextmanager
-def _TemporarilyInstallApp(device, apk):
+def _TemporarilyInstallApp(device, apk, install_timeout=None):
"""A context manager that installs an app while in scope."""
- device.Install(apk, reinstall=True)
+ if install_timeout is None:
+ device.Install(apk, reinstall=True)
+ else:
+ device.Install(apk, reinstall=True, timeout=install_timeout)
+
try:
yield
finally:
diff --git a/catapult/devil/devil/android/tools/system_app_devicetest.py b/catapult/devil/devil/android/tools/system_app_devicetest.py
index 0e8afdca..293bad19 100755
--- a/catapult/devil/devil/android/tools/system_app_devicetest.py
+++ b/catapult/devil/devil/android/tools/system_app_devicetest.py
@@ -39,7 +39,7 @@ class SystemAppDeviceTest(device_test_case.DeviceTestCase):
self._cached_apks = {}
for o in self._original_paths:
h = os.path.join(self._apk_cache_dir, posixpath.basename(o))
- self._device.PullFile(o, h)
+ self._device.PullFile(o, h, timeout=60)
self._cached_apks[h] = o
def tearDown(self):
diff --git a/catapult/devil/devil/android/tools/system_app_test.py b/catapult/devil/devil/android/tools/system_app_test.py
index 1400d7eb..44df7ead 100644
--- a/catapult/devil/devil/android/tools/system_app_test.py
+++ b/catapult/devil/devil/android/tools/system_app_test.py
@@ -21,6 +21,16 @@ with devil_env.SysPath(devil_env.PYMOCK_PATH):
import mock
+_PACKAGE_NAME = 'com.android'
+_PACKAGE_PATH = '/path/to/com.android.apk'
+_PM_LIST_PACKAGES_COMMAND = ['pm', 'list', 'packages', '-f', '-u',
+ _PACKAGE_NAME]
+_PM_LIST_PACKAGES_OUTPUT_WITH_PATH = ['package:/path/to/other=' + _PACKAGE_NAME
+ + '.other', 'package:' + _PACKAGE_PATH +
+ '=' + _PACKAGE_NAME]
+_PM_LIST_PACKAGES_OUTPUT_WITHOUT_PATH = ['package:/path/to/other=' +
+ _PACKAGE_NAME + '.other']
+
class SystemAppTest(unittest.TestCase):
def testDoubleEnableModification(self):
@@ -64,6 +74,62 @@ class SystemAppTest(unittest.TestCase):
mock_device.SetProp.assert_called_once_with(
system_app._ENABLE_MODIFICATION_PROP, '0')
+ def test_GetApplicationPaths_found(self):
+ """Path found in output along with another package having similar name."""
+ # pylint: disable=protected-access
+ mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+ mock_device.RunShellCommand.configure_mock(
+ return_value=_PM_LIST_PACKAGES_OUTPUT_WITH_PATH
+ )
+
+ paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
+
+ self.assertEquals([_PACKAGE_PATH], paths)
+ mock_device.RunShellCommand.assert_called_once_with(
+ _PM_LIST_PACKAGES_COMMAND, check_return=True)
+
+ def test_GetApplicationPaths_notFound(self):
+ """Path not found in output, only another package with similar name."""
+ # pylint: disable=protected-access
+ mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+ mock_device.RunShellCommand.configure_mock(
+ return_value=_PM_LIST_PACKAGES_OUTPUT_WITHOUT_PATH
+ )
+
+ paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
+
+ self.assertEquals([], paths)
+ mock_device.RunShellCommand.assert_called_once_with(
+ _PM_LIST_PACKAGES_COMMAND, check_return=True)
+
+ def test_GetApplicationPaths_noPaths(self):
+ """Nothing containing text of package name found in output."""
+ # pylint: disable=protected-access
+ mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+ mock_device.RunShellCommand.configure_mock(
+ return_value=[]
+ )
+
+ paths = system_app._GetApplicationPaths(mock_device, _PACKAGE_NAME)
+
+ self.assertEquals([], paths)
+ mock_device.RunShellCommand.assert_called_once_with(
+ _PM_LIST_PACKAGES_COMMAND, check_return=True)
+
+ def test_GetApplicationPaths_emptyName(self):
+ """Called with empty name, should not return any packages."""
+ # pylint: disable=protected-access
+ mock_device = mock.Mock(spec=device_utils.DeviceUtils)
+ mock_device.RunShellCommand.configure_mock(
+ return_value=_PM_LIST_PACKAGES_OUTPUT_WITH_PATH
+ )
+
+ paths = system_app._GetApplicationPaths(mock_device, '')
+
+ self.assertEquals([], paths)
+ mock_device.RunShellCommand.assert_called_once_with(
+ _PM_LIST_PACKAGES_COMMAND[:-1] + [''], check_return=True)
+
if __name__ == '__main__':
unittest.main()
diff --git a/catapult/devil/devil/android/tools/webview_app.py b/catapult/devil/devil/android/tools/webview_app.py
new file mode 100755
index 00000000..36b70391
--- /dev/null
+++ b/catapult/devil/devil/android/tools/webview_app.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A script to use a package as the WebView provider while running a command."""
+
+import argparse
+import contextlib
+import logging
+import os
+import re
+import sys
+
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..', '..', '..')))
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..', '..', '..', '..', 'common', 'py_utils')))
+
+
+from devil.android import apk_helper
+from devil.android import device_errors
+from devil.android.sdk import version_codes
+from devil.android.tools import script_common
+from devil.android.tools import system_app
+from devil.utils import cmd_helper
+from devil.utils import parallelizer
+from devil.utils import run_tests_helper
+from py_utils import tempfile_ext
+
+logger = logging.getLogger(__name__)
+
+_SYSTEM_PATH_RE = re.compile(r'^\s*\/system\/')
+_WEBVIEW_INSTALL_TIMEOUT = 300
+
+@contextlib.contextmanager
+def UseWebViewProvider(device, apk, expected_package=''):
+ """A context manager that uses the apk as the webview provider while in scope.
+
+ Args:
+ device: (device_utils.DeviceUtils) The device for which the webview apk
+ should be used as the provider.
+ apk: (str) The path to the webview APK to use.
+ expected_package: (str) If non-empty, verify apk's package name matches
+ this value.
+ """
+ package_name = apk_helper.GetPackageName(apk)
+
+ if expected_package:
+ if package_name != expected_package:
+ raise device_errors.CommandFailedError(
+ 'WebView Provider package %s does not match expected %s' %
+ (package_name, expected_package), str(device))
+
+ if (device.build_version_sdk in
+ [version_codes.NOUGAT, version_codes.NOUGAT_MR1]):
+ logger.warning('Due to webviewupdate bug in Nougat, WebView Fallback Logic '
+ 'will be disabled and WebView provider may be changed after '
+ 'exit of UseWebViewProvider context manager scope.')
+
+ webview_update = device.GetWebViewUpdateServiceDump()
+ original_fallback_logic = webview_update.get('FallbackLogicEnabled', None)
+ original_provider = webview_update.get('CurrentWebViewPackage', None)
+
+ # This is only necessary if the provider is a fallback provider, but we can't
+ # generally determine this, so we set this just in case.
+ device.SetWebViewFallbackLogic(False)
+
+ try:
+ # If user installed versions of the package is present, they must be
+ # uninstalled first, so that the system version of the package,
+ # if any, can be found by the ReplaceSystemApp context manager
+ with _UninstallNonSystemApp(device, package_name):
+ all_paths = device.GetApplicationPaths(package_name)
+ system_paths = _FilterPaths(all_paths, True)
+ non_system_paths = _FilterPaths(all_paths, False)
+ if non_system_paths:
+ raise device_errors.CommandFailedError(
+ 'Non-System application paths found after uninstallation: ',
+ str(non_system_paths))
+ elif system_paths:
+ # app is system app, use ReplaceSystemApp to install
+ with system_app.ReplaceSystemApp(
+ device,
+ package_name,
+ apk,
+ install_timeout=_WEBVIEW_INSTALL_TIMEOUT):
+ _SetWebViewProvider(device, package_name)
+ yield
+ else:
+ # app is not present on device, can directly install
+ with _InstallApp(device, apk):
+ _SetWebViewProvider(device, package_name)
+ yield
+ finally:
+ # restore the original provider only if it was known and not the current
+ # provider
+ if original_provider is not None:
+ webview_update = device.GetWebViewUpdateServiceDump()
+ new_provider = webview_update.get('CurrentWebViewPackage', None)
+ if new_provider != original_provider:
+ device.SetWebViewImplementation(original_provider)
+
+ # enable the fallback logic only if it was known to be enabled
+ if original_fallback_logic is True:
+ device.SetWebViewFallbackLogic(True)
+
+
+def _SetWebViewProvider(device, package_name):
+ """ Set the WebView provider to the package_name if supported. """
+ if device.build_version_sdk >= version_codes.NOUGAT:
+ device.SetWebViewImplementation(package_name)
+
+
+def _FilterPaths(path_list, is_system):
+ """ Return paths in the path_list that are/aren't system paths. """
+ return [
+ p for p in path_list if is_system == bool(re.match(_SYSTEM_PATH_RE, p))
+ ]
+
+
+def _RebasePath(new_root, old_root):
+ """ Graft old_root onto new_root and return the result. """
+ return os.path.join(new_root, os.path.relpath(old_root, '/'))
+
+
+@contextlib.contextmanager
+def _UninstallNonSystemApp(device, package_name):
+ """ Make package un-installed while in scope. """
+ all_paths = device.GetApplicationPaths(package_name)
+ user_paths = _FilterPaths(all_paths, False)
+ host_paths = []
+ if user_paths:
+ with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
+ for user_path in user_paths:
+ host_path = _RebasePath(temp_dir, user_path)
+ # PullFile takes care of host_path creation if needed.
+ device.PullFile(user_path, host_path)
+ host_paths.append(host_path)
+ device.Uninstall(package_name)
+ try:
+ yield
+ finally:
+ for host_path in reversed(host_paths):
+ device.Install(host_path, reinstall=True,
+ timeout=_WEBVIEW_INSTALL_TIMEOUT)
+ else:
+ yield
+
+
+@contextlib.contextmanager
+def _InstallApp(device, apk):
+ """ Make apk installed while in scope. """
+ package_name = apk_helper.GetPackageName(apk)
+ device.Install(apk, reinstall=True, timeout=_WEBVIEW_INSTALL_TIMEOUT)
+ try:
+ yield
+ finally:
+ device.Uninstall(package_name)
+
+
+def main(raw_args):
+ parser = argparse.ArgumentParser()
+
+ def add_common_arguments(p):
+ script_common.AddDeviceArguments(p)
+ script_common.AddEnvironmentArguments(p)
+ p.add_argument(
+ '-v', '--verbose', action='count', default=0,
+ help='Print more information.')
+ p.add_argument('command', nargs='*')
+
+ @contextlib.contextmanager
+ def use_webview_provider(device, args):
+ with UseWebViewProvider(device, args.apk, args.expected_package):
+ yield
+
+ parser.add_argument(
+ '--apk', required=True,
+ help='The apk to use as the provider.')
+ parser.add_argument(
+ '--expected-package', default='',
+ help="Verify apk's package name matches value, disabled by default.")
+ add_common_arguments(parser)
+ parser.set_defaults(func=use_webview_provider)
+
+ args = parser.parse_args(raw_args)
+
+ run_tests_helper.SetLogLevel(args.verbose)
+ script_common.InitializeEnvironment(args)
+
+ devices = script_common.GetDevices(args.devices, args.blacklist_file)
+ parallel_devices = parallelizer.SyncParallelizer(
+ [args.func(d, args) for d in devices])
+ with parallel_devices:
+ if args.command:
+ return cmd_helper.Call(args.command)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/catapult/devil/devil/devil_dependencies.json b/catapult/devil/devil/devil_dependencies.json
index 8a7943e2..8b397882 100644
--- a/catapult/devil/devil/devil_dependencies.json
+++ b/catapult/devil/devil/devil_dependencies.json
@@ -6,7 +6,7 @@
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
- "cloud_storage_hash": "16ba3180141a2489d7ec99b39fd6e3434a9a373f",
+ "cloud_storage_hash": "87bd288daab30624e41faa62aa2c1d5bac3e60aa",
"download_path": "../bin/deps/linux2/x86_64/bin/aapt"
}
}
@@ -26,8 +26,8 @@
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
- "cloud_storage_hash": "91cdce1e3bd81b2ac1fd380013896d0e2cdb40a0",
- "download_path": "../bin/deps/linux2/x86_64/lib/libc++.so"
+ "cloud_storage_hash": "9b986774ad27288a6777ebfa9a08fd8a52003008",
+ "download_path": "../bin/deps/linux2/x86_64/lib64/libc++.so"
}
}
},
@@ -46,7 +46,7 @@
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
- "cloud_storage_hash": "acfb10f7a868baf9bcf446a2d9f8ed6b5d52c3c6",
+ "cloud_storage_hash": "c3fdf75afe8eb4062d66703cb556ee1e2064b8ae",
"download_path": "../bin/deps/linux2/x86_64/bin/dexdump"
}
}
@@ -55,13 +55,13 @@
"cloud_storage_base_folder": "binary_dependencies",
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
- "android_armeabi-v7a": {
- "cloud_storage_hash": "220ff3ba1a6c3c81877997e32784ffd008f293a5",
- "download_path": "../bin/deps/android/armeabi-v7a/apks/EmptySystemWebView.apk"
- },
"android_arm64-v8a": {
"cloud_storage_hash": "34e583c631a495afbba82ce8a1d4f9b5118a4411",
"download_path": "../bin/deps/android/arm64-v8a/apks/EmptySystemWebView.apk"
+ },
+ "android_armeabi-v7a": {
+ "cloud_storage_hash": "220ff3ba1a6c3c81877997e32784ffd008f293a5",
+ "download_path": "../bin/deps/android/armeabi-v7a/apks/EmptySystemWebView.apk"
}
}
},
@@ -132,7 +132,7 @@
"cloud_storage_bucket": "chromium-telemetry",
"file_info": {
"linux2_x86_64": {
- "cloud_storage_hash": "abb9753a8d3efeea4144e328933931729e01571c",
+ "cloud_storage_hash": "c116fd0d7ff089561971c078317b75b90f053207",
"download_path": "../bin/deps/linux2/x86_64/bin/split-select"
}
}
diff --git a/catapult/devil/devil/utils/cmd_helper.py b/catapult/devil/devil/utils/cmd_helper.py
index b7b2f0dc..3c4a06ed 100644
--- a/catapult/devil/devil/utils/cmd_helper.py
+++ b/catapult/devil/devil/utils/cmd_helper.py
@@ -4,6 +4,7 @@
"""A wrapper for subprocess to make calling shell commands easier."""
+import codecs
import logging
import os
import pipes
@@ -15,11 +16,16 @@ import subprocess
import sys
import time
+from devil import base_error
logger = logging.getLogger(__name__)
_SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
+# Cache the string-escape codec to ensure subprocess can find it
+# later. Return value doesn't matter.
+codecs.lookup('string-escape')
+
def SingleQuote(s):
"""Return an shell-escaped version of the string using single quotes.
@@ -231,11 +237,11 @@ def GetCmdStatusOutputAndError(args, cwd=None, shell=False, env=None):
return (pipe.returncode, stdout, stderr)
-class TimeoutError(Exception):
+class TimeoutError(base_error.BaseError):
"""Module-specific timeout exception."""
def __init__(self, output=None):
- super(TimeoutError, self).__init__()
+ super(TimeoutError, self).__init__('Timeout')
self._output = output
@property
diff --git a/catapult/devil/devil/utils/logging_common.py b/catapult/devil/devil/utils/logging_common.py
index 5aea3c64..ab364a20 100644
--- a/catapult/devil/devil/utils/logging_common.py
+++ b/catapult/devil/devil/utils/logging_common.py
@@ -8,13 +8,32 @@ import time
def AddLoggingArguments(parser):
- parser.add_argument(
+ """Adds standard logging flags to the parser.
+
+ After parsing args, remember to invoke InitializeLogging() with the parsed
+ args, to configure the log level.
+ """
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
'-v', '--verbose', action='count', default=0,
help='Log more. Use multiple times for even more logging.')
+ group.add_argument(
+ '-q', '--quiet', action='count', default=0,
+ help=('Log less (suppress output). Use multiple times for even less '
+ 'output.'))
def InitializeLogging(args, handler=None):
- if args.verbose == 0:
+ """Initialized the log level based on commandline flags.
+
+ This expects to be given an "args" object with the options defined by
+ AddLoggingArguments().
+ """
+ if args.quiet >= 2:
+ log_level = logging.CRITICAL
+ elif args.quiet == 1:
+ log_level = logging.ERROR
+ elif args.verbose == 0:
log_level = logging.WARNING
elif args.verbose == 1:
log_level = logging.INFO
diff --git a/catapult/devil/devil/utils/markdown.py b/catapult/devil/devil/utils/markdown.py
index 6867e9db..ba666643 100755
--- a/catapult/devil/devil/utils/markdown.py
+++ b/catapult/devil/devil/utils/markdown.py
@@ -3,6 +3,8 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+from __future__ import print_function
+
import argparse
import imp
import os
@@ -221,7 +223,7 @@ def md_module(module_obj, module_path=None, module_link=None):
for f in functions_to_doc:
content += md_function(f)
- print '\n'.join(content)
+ print('\n'.join(content))
return 0
diff --git a/catapult/devil/devil/utils/reraiser_thread.py b/catapult/devil/devil/utils/reraiser_thread.py
index cb9764ef..6e6c810b 100644
--- a/catapult/devil/devil/utils/reraiser_thread.py
+++ b/catapult/devil/devil/utils/reraiser_thread.py
@@ -11,12 +11,14 @@ import threading
import time
import traceback
+from devil import base_error
from devil.utils import watchdog_timer
-class TimeoutError(Exception):
+class TimeoutError(base_error.BaseError):
"""Module-specific timeout exception."""
- pass
+ def __init__(self, message):
+ super(TimeoutError, self).__init__(message)
def LogThreadStack(thread, error_log_func=logging.critical):
@@ -67,10 +69,17 @@ class ReraiserThread(threading.Thread):
self._exc_info = None
self._thread_group = None
- def ReraiseIfException(self):
- """Reraise exception if an exception was raised in the thread."""
- if self._exc_info:
- raise self._exc_info[0], self._exc_info[1], self._exc_info[2]
+ if sys.version_info < (3,):
+ # pylint: disable=exec-used
+ exec('''def ReraiseIfException(self):
+ """Reraise exception if an exception was raised in the thread."""
+ if self._exc_info:
+ raise self._exc_info[0], self._exc_info[1], self._exc_info[2]''')
+ else:
+ def ReraiseIfException(self):
+ """Reraise exception if an exception was raised in the thread."""
+ if self._exc_info:
+ raise self._exc_info[1]
def GetReturnValue(self):
"""Reraise exception if present, otherwise get the return value."""
diff --git a/catapult/devil/devil/utils/reset_usb.py b/catapult/devil/devil/utils/reset_usb.py
index 0335227d..404a44c6 100755
--- a/catapult/devil/devil/utils/reset_usb.py
+++ b/catapult/devil/devil/utils/reset_usb.py
@@ -3,12 +3,15 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import sys
+if sys.platform == 'win32':
+ raise ImportError('devil.utils.reset_usb only supported on unix systems.')
+
import argparse
import fcntl
import logging
import os
import re
-import sys
if __name__ == '__main__':
sys.path.append(
diff --git a/catapult/devil/devil/utils/run_tests_helper.py b/catapult/devil/devil/utils/run_tests_helper.py
index 7f71b65c..0b9dd47f 100644
--- a/catapult/devil/devil/utils/run_tests_helper.py
+++ b/catapult/devil/devil/utils/run_tests_helper.py
@@ -14,7 +14,7 @@ CustomFormatter = logging_common.CustomFormatter
_WrappedLoggingArgs = collections.namedtuple(
- '_WrappedLoggingArgs', ['verbose'])
+ '_WrappedLoggingArgs', ['verbose', 'quiet'])
def SetLogLevel(verbose_count, add_handler=True):
@@ -25,5 +25,5 @@ def SetLogLevel(verbose_count, add_handler=True):
add_handler: If true, adds a handler with |CustomFormatter|.
"""
logging_common.InitializeLogging(
- _WrappedLoggingArgs(verbose_count),
+ _WrappedLoggingArgs(verbose_count, 0),
handler=None if add_handler else logging.NullHandler())