| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 """Provides an interface to communicate with the device via the adb command. | |
| 7 | |
| 8 Assumes adb binary is currently on system path. | |
| 9 | |
| 10 Usage: | |
| 11 python android_commands.py wait-for-pm | |
| 12 """ | |
| 13 | |
| 14 import collections | |
| 15 import datetime | |
| 16 import logging | |
| 17 import optparse | |
| 18 import os | |
| 19 import pexpect | |
| 20 import re | |
| 21 import subprocess | |
| 22 import sys | |
| 23 import tempfile | |
| 24 import time | |
| 25 | |
| 26 # adb_interface.py is under ../../third_party/android_testrunner/ | |
| 27 sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', | |
| 28 '..', 'third_party', 'android_testrunner')) | |
| 29 import adb_interface | |
| 30 import cmd_helper | |
| 31 import errors # is under ../../third_party/android_testrunner/errors.py | |
| 32 from run_tests_helper import IsRunningAsBuildbot | |
| 33 | |
| 34 | |
| 35 # Pattern to search for the next whole line of pexpect output and capture it | |
| 36 # into a match group. We can't use ^ and $ for line start end with pexpect, | |
| 37 # see http://www.noah.org/python/pexpect/#doc for explanation why. | |
| 38 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') | |
| 39 | |
| 40 # Set the adb shell prompt to be a unique marker that will [hopefully] not | |
| 41 # appear at the start of any line of a command's output. | |
| 42 SHELL_PROMPT = '~+~PQ\x17RS~+~' | |
| 43 | |
| 44 # This only works for single core devices. | |
| 45 SCALING_GOVERNOR = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor' | |
| 46 DROP_CACHES = '/proc/sys/vm/drop_caches' | |
| 47 | |
| 48 # Java properties file | |
| 49 LOCAL_PROPERTIES_PATH = '/data/local.prop' | |
| 50 | |
| 51 # Property in /data/local.prop that controls Java assertions. | |
| 52 JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions' | |
| 53 | |
| 54 BOOT_COMPLETE_RE = re.compile( | |
| 55 re.escape('android.intent.action.MEDIA_MOUNTED path: /mnt/sdcard') | |
| 56 + '|' + re.escape('PowerManagerService: bootCompleted')) | |
| 57 | |
| 58 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). | |
| 59 KEYCODE_DPAD_RIGHT = 22 | |
| 60 KEYCODE_ENTER = 66 | |
| 61 KEYCODE_MENU = 82 | |
| 62 KEYCODE_BACK = 4 | |
| 63 | |
| 64 | |
| 65 def GetEmulators(): | |
| 66 """Returns a list of emulators. Does not filter by status (e.g. offline). | |
| 67 | |
| 68 Both devices starting with 'emulator' will be returned in below output: | |
| 69 | |
| 70 * daemon not running. starting it now on port 5037 * | |
| 71 * daemon started successfully * | |
| 72 List of devices attached | |
| 73 027c10494100b4d7 device | |
| 74 emulator-5554 offline | |
| 75 emulator-5558 device | |
| 76 """ | |
| 77 re_device = re.compile('^emulator-[0-9]+', re.MULTILINE) | |
| 78 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) | |
| 79 return devices | |
| 80 | |
| 81 | |
| 82 def GetAttachedDevices(): | |
| 83 """Returns a list of attached, online android devices. | |
| 84 | |
| 85 If a preferred device has been set with ANDROID_SERIAL, it will be first in | |
| 86 the returned list. | |
| 87 | |
| 88 Example output: | |
| 89 | |
| 90 * daemon not running. starting it now on port 5037 * | |
| 91 * daemon started successfully * | |
| 92 List of devices attached | |
| 93 027c10494100b4d7 device | |
| 94 emulator-5554 offline | |
| 95 """ | |
| 96 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) | |
| 97 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) | |
| 98 preferred_device = os.environ.get("ANDROID_SERIAL") | |
| 99 if preferred_device in devices: | |
| 100 devices.remove(preferred_device) | |
| 101 devices.insert(0, preferred_device) | |
| 102 return devices | |
| 103 | |
| 104 | |
| 105 def _GetHostFileInfo(file_name): | |
| 106 """Returns a tuple containing size and modified UTC time for file_name.""" | |
| 107 # The time accuracy on device is only to minute level, remove the second and | |
| 108 # microsecond from host results. | |
| 109 utc_time = datetime.datetime.utcfromtimestamp(os.path.getmtime(file_name)) | |
| 110 time_delta = datetime.timedelta(seconds=utc_time.second, | |
| 111 microseconds=utc_time.microsecond) | |
| 112 return os.path.getsize(file_name), utc_time - time_delta | |
| 113 | |
| 114 | |
| 115 def ListHostPathContents(path): | |
| 116 """Lists files in all subdirectories of |path|. | |
| 117 | |
| 118 Args: | |
| 119 path: The path to list. | |
| 120 | |
| 121 Returns: | |
| 122 A dict of {"name": (size, lastmod), ...}. | |
| 123 """ | |
| 124 if os.path.isfile(path): | |
| 125 return {os.path.basename(path): _GetHostFileInfo(path)} | |
| 126 ret = {} | |
| 127 for root, dirs, files in os.walk(path): | |
| 128 for d in dirs: | |
| 129 if d.startswith('.'): | |
| 130 dirs.remove(d) # Prune the dir for subsequent iterations. | |
| 131 for f in files: | |
| 132 if f.startswith('.'): | |
| 133 continue | |
| 134 full_file_name = os.path.join(root, f) | |
| 135 file_name = os.path.relpath(full_file_name, path) | |
| 136 ret[file_name] = _GetHostFileInfo(full_file_name) | |
| 137 return ret | |
| 138 | |
| 139 | |
| 140 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): | |
| 141 """Gets a list of files from `ls` command output. | |
| 142 | |
| 143 Python's os.walk isn't used because it doesn't work over adb shell. | |
| 144 | |
| 145 Args: | |
| 146 path: The path to list. | |
| 147 ls_output: A list of lines returned by an `ls -lR` command. | |
| 148 re_file: A compiled regular expression which parses a line into named groups | |
| 149 consisting of at minimum "filename", "date", "time", "size" and | |
| 150 optionally "timezone". | |
| 151 utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a | |
| 152 2-digit string giving the number of UTC offset hours, and MM is a | |
| 153 2-digit string giving the number of UTC offset minutes. If the input | |
| 154 utc_offset is None, will try to look for the value of "timezone" if it | |
| 155 is specified in re_file. | |
| 156 | |
| 157 Returns: | |
| 158 A dict of {"name": (size, lastmod), ...} where: | |
| 159 name: The file name relative to |path|'s directory. | |
| 160 size: The file size in bytes (0 for directories). | |
| 161 lastmod: The file last modification date in UTC. | |
| 162 """ | |
| 163 re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path)) | |
| 164 path_dir = os.path.dirname(path) | |
| 165 | |
| 166 current_dir = '' | |
| 167 files = {} | |
| 168 for line in ls_output: | |
| 169 directory_match = re_directory.match(line) | |
| 170 if directory_match: | |
| 171 current_dir = directory_match.group('dir') | |
| 172 continue | |
| 173 file_match = re_file.match(line) | |
| 174 if file_match: | |
| 175 filename = os.path.join(current_dir, file_match.group('filename')) | |
| 176 if filename.startswith(path_dir): | |
| 177 filename = filename[len(path_dir)+1:] | |
| 178 lastmod = datetime.datetime.strptime( | |
| 179 file_match.group('date') + ' ' + file_match.group('time')[:5], | |
| 180 '%Y-%m-%d %H:%M') | |
| 181 if not utc_offset and 'timezone' in re_file.groupindex: | |
| 182 utc_offset = file_match.group('timezone') | |
| 183 if isinstance(utc_offset, str) and len(utc_offset) == 5: | |
| 184 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), | |
| 185 minutes=int(utc_offset[3:5])) | |
| 186 if utc_offset[0:1] == '-': | |
| 187 utc_delta = -utc_delta; | |
| 188 lastmod -= utc_delta | |
| 189 files[filename] = (int(file_match.group('size')), lastmod) | |
| 190 return files | |
| 191 | |
| 192 | |
| 193 def GetLogTimestamp(log_line): | |
| 194 """Returns the timestamp of the given |log_line|.""" | |
| 195 try: | |
| 196 return datetime.datetime.strptime(log_line[:18], '%m-%d %H:%M:%S.%f') | |
| 197 except (ValueError, IndexError): | |
| 198 logging.critical('Error reading timestamp from ' + log_line) | |
| 199 return None | |
| 200 | |
| 201 | |
| 202 class AndroidCommands(object): | |
| 203 """Helper class for communicating with Android device via adb. | |
| 204 | |
| 205 Args: | |
| 206 device: If given, adb commands are only send to the device of this ID. | |
| 207 Otherwise commands are sent to all attached devices. | |
| 208 wait_for_pm: If true, issues an adb wait-for-device command. | |
| 209 """ | |
| 210 | |
| 211 def __init__(self, device=None, wait_for_pm=False): | |
| 212 self._adb = adb_interface.AdbInterface() | |
| 213 if device: | |
| 214 self._adb.SetTargetSerial(device) | |
| 215 if wait_for_pm: | |
| 216 self.WaitForDevicePm() | |
| 217 self._logcat = None | |
| 218 self._original_governor = None | |
| 219 self._pushed_files = [] | |
| 220 | |
| 221 def Adb(self): | |
| 222 """Returns our AdbInterface to avoid us wrapping all its methods.""" | |
| 223 return self._adb | |
| 224 | |
| 225 def WaitForDevicePm(self): | |
| 226 """Blocks until the device's package manager is available. | |
| 227 | |
| 228 To workaround http://b/5201039, we restart the shell and retry if the | |
| 229 package manager isn't back after 120 seconds. | |
| 230 | |
| 231 Raises: | |
| 232 errors.WaitForResponseTimedOutError after max retries reached. | |
| 233 """ | |
| 234 last_err = None | |
| 235 retries = 3 | |
| 236 while retries: | |
| 237 try: | |
| 238 self._adb.WaitForDevicePm() | |
| 239 return # Success | |
| 240 except errors.WaitForResponseTimedOutError as e: | |
| 241 last_err = e | |
| 242 logging.warning('Restarting and retrying after timeout: %s' % str(e)) | |
| 243 retries -= 1 | |
| 244 self.RestartShell() | |
| 245 raise last_err # Only reached after max retries, re-raise the last error. | |
| 246 | |
| 247 def SynchronizeDateTime(self): | |
| 248 """Synchronize date/time between host and device.""" | |
| 249 self._adb.SendShellCommand('date -u %f' % time.time()) | |
| 250 | |
| 251 def RestartShell(self): | |
| 252 """Restarts the shell on the device. Does not block for it to return.""" | |
| 253 self.RunShellCommand('stop') | |
| 254 self.RunShellCommand('start') | |
| 255 | |
| 256 def Reboot(self, full_reboot=True): | |
| 257 """Reboots the device and waits for the package manager to return. | |
| 258 | |
| 259 Args: | |
| 260 full_reboot: Whether to fully reboot the device or just restart the shell. | |
| 261 """ | |
| 262 # TODO(torne): hive can't reboot the device either way without breaking the | |
| 263 # connection; work out if we can handle this better | |
| 264 if os.environ.get('USING_HIVE'): | |
| 265 logging.warning('Ignoring reboot request as we are on hive') | |
| 266 return | |
| 267 if full_reboot: | |
| 268 self._adb.SendCommand('reboot') | |
| 269 else: | |
| 270 self.RestartShell() | |
| 271 self.WaitForDevicePm() | |
| 272 self.StartMonitoringLogcat(timeout=120) | |
| 273 self.WaitForLogMatch(BOOT_COMPLETE_RE) | |
| 274 self.UnlockDevice() | |
| 275 | |
| 276 def Uninstall(self, package): | |
| 277 """Uninstalls the specified package from the device. | |
| 278 | |
| 279 Args: | |
| 280 package: Name of the package to remove. | |
| 281 """ | |
| 282 uninstall_command = 'uninstall %s' % package | |
| 283 | |
| 284 logging.info('>>> $' + uninstall_command) | |
| 285 self._adb.SendCommand(uninstall_command, timeout_time=60) | |
| 286 | |
| 287 def Install(self, package_file_path): | |
| 288 """Installs the specified package to the device. | |
| 289 | |
| 290 Args: | |
| 291 package_file_path: Path to .apk file to install. | |
| 292 """ | |
| 293 | |
| 294 assert os.path.isfile(package_file_path) | |
| 295 | |
| 296 install_command = 'install %s' % package_file_path | |
| 297 | |
| 298 logging.info('>>> $' + install_command) | |
| 299 self._adb.SendCommand(install_command, timeout_time=2*60) | |
| 300 | |
| 301 # It is tempting to turn this function into a generator, however this is not | |
| 302 # possible without using a private (local) adb_shell instance (to ensure no | |
| 303 # other command interleaves usage of it), which would defeat the main aim of | |
| 304 # being able to reuse the adb shell instance across commands. | |
| 305 def RunShellCommand(self, command, timeout_time=20, log_result=True): | |
| 306 """Send a command to the adb shell and return the result. | |
| 307 | |
| 308 Args: | |
| 309 command: String containing the shell command to send. Must not include | |
| 310 the single quotes as we use them to escape the whole command. | |
| 311 timeout_time: Number of seconds to wait for command to respond before | |
| 312 retrying, used by AdbInterface.SendShellCommand. | |
| 313 log_result: Boolean to indicate whether we should log the result of the | |
| 314 shell command. | |
| 315 | |
| 316 Returns: | |
| 317 list containing the lines of output received from running the command | |
| 318 """ | |
| 319 logging.info('>>> $' + command) | |
| 320 if "'" in command: logging.warning(command + " contains ' quotes") | |
| 321 result = self._adb.SendShellCommand("'%s'" % command, | |
| 322 timeout_time).splitlines() | |
| 323 if log_result: | |
| 324 logging.info('\n>>> '.join(result)) | |
| 325 return result | |
| 326 | |
| 327 def KillAll(self, process): | |
| 328 """Android version of killall, connected via adb. | |
| 329 | |
| 330 Args: | |
| 331 process: name of the process to kill off | |
| 332 | |
| 333 Returns: | |
| 334 the number of processess killed | |
| 335 """ | |
| 336 pids = self.ExtractPid(process) | |
| 337 if pids: | |
| 338 self.RunShellCommand('kill ' + ' '.join(pids)) | |
| 339 return len(pids) | |
| 340 | |
| 341 def StartActivity(self, package, activity, | |
| 342 action='android.intent.action.VIEW', data=None, | |
| 343 extras=None, trace_file_name=None): | |
| 344 """Starts |package|'s activity on the device. | |
| 345 | |
| 346 Args: | |
| 347 package: Name of package to start (e.g. 'com.android.chrome'). | |
| 348 activity: Name of activity (e.g. '.Main' or 'com.android.chrome.Main'). | |
| 349 data: Data string to pass to activity (e.g. 'http://www.example.com/'). | |
| 350 extras: Dict of extras to pass to activity. | |
| 351 trace_file_name: If used, turns on and saves the trace to this file name. | |
| 352 """ | |
| 353 cmd = 'am start -a %s -n %s/%s' % (action, package, activity) | |
| 354 if data: | |
| 355 cmd += ' -d "%s"' % data | |
| 356 if extras: | |
| 357 cmd += ' -e' | |
| 358 for key in extras: | |
| 359 cmd += ' %s %s' % (key, extras[key]) | |
| 360 if trace_file_name: | |
| 361 cmd += ' -S -P ' + trace_file_name | |
| 362 self.RunShellCommand(cmd) | |
| 363 | |
| 364 def EnableAdbRoot(self): | |
| 365 """Enable root on the device.""" | |
| 366 self._adb.EnableAdbRoot() | |
| 367 | |
| 368 def CloseApplication(self, package): | |
| 369 """Attempt to close down the application, using increasing violence. | |
| 370 | |
| 371 Args: | |
| 372 package: Name of the process to kill off, e.g. com.android.chrome | |
| 373 """ | |
| 374 self.RunShellCommand('am force-stop ' + package) | |
| 375 | |
| 376 def ClearApplicationState(self, package): | |
| 377 """Closes and clears all state for the given |package|.""" | |
| 378 self.CloseApplication(package) | |
| 379 self.RunShellCommand('rm -r /data/data/%s/cache/*' % package) | |
| 380 self.RunShellCommand('rm -r /data/data/%s/files/*' % package) | |
| 381 self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package) | |
| 382 | |
| 383 def SendKeyEvent(self, keycode): | |
| 384 """Sends keycode to the device. | |
| 385 | |
| 386 Args: | |
| 387 keycode: Numeric keycode to send (see "enum" at top of file). | |
| 388 """ | |
| 389 self.RunShellCommand('input keyevent %d' % keycode) | |
| 390 | |
| 391 def PushIfNeeded(self, local_path, device_path): | |
| 392 """Pushes |local_path| to |device_path|. | |
| 393 | |
| 394 Works for files and directories. This method skips copying any paths in | |
| 395 |test_data_paths| that already exist on the device with the same timestamp | |
| 396 and size. | |
| 397 | |
| 398 All pushed files can be removed by calling RemovePushedFiles(). | |
| 399 """ | |
| 400 assert os.path.exists(local_path), 'Local path not found %s' % local_path | |
| 401 self._pushed_files.append(device_path) | |
| 402 | |
| 403 # If the path contents are the same, there's nothing to do. | |
| 404 local_contents = ListHostPathContents(local_path) | |
| 405 device_contents = self.ListPathContents(device_path) | |
| 406 # Only compare the size and timestamp if only copying a file because | |
| 407 # the filename on device can be renamed. | |
| 408 if os.path.isfile(local_path): | |
| 409 assert len(local_contents) == 1 | |
| 410 is_equal = local_contents.values() == device_contents.values() | |
| 411 else: | |
| 412 is_equal = local_contents == device_contents | |
| 413 if is_equal: | |
| 414 logging.info('%s is up-to-date. Skipping file push.' % device_path) | |
| 415 return | |
| 416 | |
| 417 # They don't match, so remove everything first and then create it. | |
| 418 if os.path.isdir(local_path): | |
| 419 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) | |
| 420 self.RunShellCommand('mkdir -p %s' % device_path) | |
| 421 | |
| 422 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of | |
| 423 # 60 seconds which isn't sufficient for a lot of users of this method. | |
| 424 push_command = 'push %s %s' % (local_path, device_path) | |
| 425 logging.info('>>> $' + push_command) | |
| 426 output = self._adb.SendCommand(push_command, timeout_time=30*60) | |
| 427 # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" | |
| 428 # Errors look like this: "failed to copy ... " | |
| 429 if not re.search('^[0-9]', output): | |
| 430 logging.critical('PUSH FAILED: ' + output) | |
| 431 | |
| 432 def GetFileContents(self, filename): | |
| 433 """Gets contents from the file specified by |filename|.""" | |
| 434 return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' + | |
| 435 filename + '"; fi') | |
| 436 | |
| 437 def SetFileContents(self, filename, contents): | |
| 438 """Writes |contents| to the file specified by |filename|.""" | |
| 439 with tempfile.NamedTemporaryFile() as f: | |
| 440 f.write(contents) | |
| 441 f.flush() | |
| 442 self._adb.Push(f.name, filename) | |
| 443 | |
| 444 def RemovePushedFiles(self): | |
| 445 """Removes all files pushed with PushIfNeeded() from the device.""" | |
| 446 for p in self._pushed_files: | |
| 447 self.RunShellCommand('rm -r %s' % p, timeout_time=2*60) | |
| 448 | |
| 449 def ListPathContents(self, path): | |
| 450 """Lists files in all subdirectories of |path|. | |
| 451 | |
| 452 Args: | |
| 453 path: The path to list. | |
| 454 | |
| 455 Returns: | |
| 456 A dict of {"name": (size, lastmod), ...}. | |
| 457 """ | |
| 458 # Example output: | |
| 459 # /foo/bar: | |
| 460 # -rw-r----- 1 user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt | |
| 461 re_file = re.compile('^-(?P<perms>[^\s]+)\s+' | |
| 462 '(?P<user>[^\s]+)\s+' | |
| 463 '(?P<group>[^\s]+)\s+' | |
| 464 '(?P<size>[^\s]+)\s+' | |
| 465 '(?P<date>[^\s]+)\s+' | |
| 466 '(?P<time>[^\s]+)\s+' | |
| 467 '(?P<filename>[^\s]+)$') | |
| 468 return _GetFilesFromRecursiveLsOutput( | |
| 469 path, self.RunShellCommand('ls -lR %s' % path), re_file, | |
| 470 self.RunShellCommand('date +%z')[0]) | |
| 471 | |
| 472 def SetupPerformanceTest(self): | |
| 473 """Sets up performance tests.""" | |
| 474 # Disable CPU scaling to reduce noise in tests | |
| 475 if not self._original_governor: | |
| 476 self._original_governor = self.RunShellCommand('cat ' + SCALING_GOVERNOR) | |
| 477 self.RunShellCommand('echo performance > ' + SCALING_GOVERNOR) | |
| 478 self.DropRamCaches() | |
| 479 | |
| 480 def TearDownPerformanceTest(self): | |
| 481 """Tears down performance tests.""" | |
| 482 if self._original_governor: | |
| 483 self.RunShellCommand('echo %s > %s' % (self._original_governor[0], | |
| 484 SCALING_GOVERNOR)) | |
| 485 self._original_governor = None | |
| 486 | |
| 487 def SetJavaAssertsEnabled(self, enable): | |
| 488 """Sets or removes the device java assertions property. | |
| 489 | |
| 490 Args: | |
| 491 enable: If True the property will be set. | |
| 492 | |
| 493 Returns: | |
| 494 True if the file was modified (reboot is required for it to take effect). | |
| 495 """ | |
| 496 # First ensure the desired property is persisted. | |
| 497 temp_props_file = tempfile.NamedTemporaryFile() | |
| 498 properties = '' | |
| 499 if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name): | |
| 500 properties = file(temp_props_file.name).read() | |
| 501 re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + | |
| 502 r'\s*=\s*all\s*$', re.MULTILINE) | |
| 503 if enable != bool(re.search(re_search, properties)): | |
| 504 re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) + | |
| 505 r'\s*=\s*\w+\s*$', re.MULTILINE) | |
| 506 properties = re.sub(re_replace, '', properties) | |
| 507 if enable: | |
| 508 properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY | |
| 509 | |
| 510 file(temp_props_file.name, 'w').write(properties) | |
| 511 self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH) | |
| 512 | |
| 513 # Next, check the current runtime value is what we need, and | |
| 514 # if not, set it and report that a reboot is required. | |
| 515 was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY) | |
| 516 if was_set == enable: | |
| 517 return False | |
| 518 | |
| 519 self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY, | |
| 520 enable and 'all' or '')) | |
| 521 return True | |
| 522 | |
| 523 def DropRamCaches(self): | |
| 524 """Drops the filesystem ram caches for performance testing.""" | |
| 525 self.RunShellCommand('echo 3 > ' + DROP_CACHES) | |
| 526 | |
| 527 def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None, | |
| 528 filters=[]): | |
| 529 """Starts monitoring the output of logcat, for use with WaitForLogMatch. | |
| 530 | |
| 531 Args: | |
| 532 clear: If True the existing logcat output will be cleared, to avoiding | |
| 533 matching historical output lurking in the log. | |
| 534 timeout: How long WaitForLogMatch will wait for the given match | |
| 535 filters: A list of logcat filters to be used. | |
| 536 """ | |
| 537 if clear: | |
| 538 self.RunShellCommand('logcat -c') | |
| 539 args = ['logcat', '-v', 'threadtime'] | |
| 540 if filters: | |
| 541 args.extend(filters) | |
| 542 else: | |
| 543 args.append('*:v') | |
| 544 | |
| 545 # Spawn logcat and syncronize with it. | |
| 546 for _ in range(4): | |
| 547 self._logcat = pexpect.spawn('adb', args, timeout=timeout, | |
| 548 logfile=logfile) | |
| 549 self.RunShellCommand('log startup_sync') | |
| 550 if self._logcat.expect(['startup_sync', pexpect.EOF, | |
| 551 pexpect.TIMEOUT]) == 0: | |
| 552 break | |
| 553 self._logcat.close(force=True) | |
| 554 else: | |
| 555 logging.critical('Error reading from logcat: ' + str(self._logcat.match)) | |
| 556 sys.exit(1) | |
| 557 | |
| 558 def GetMonitoredLogCat(self): | |
| 559 """Returns an "adb logcat" command as created by pexpected.spawn.""" | |
| 560 if not self._logcat: | |
| 561 self.StartMonitoringLogcat(clear=False) | |
| 562 return self._logcat | |
| 563 | |
| 564 def WaitForLogMatch(self, search_re): | |
| 565 """Blocks until a line containing |line_re| is logged or a timeout occurs. | |
| 566 | |
| 567 Args: | |
| 568 search_re: The compiled re to search each line for. | |
| 569 | |
| 570 Returns: | |
| 571 The re match object. | |
| 572 """ | |
| 573 if not self._logcat: | |
| 574 self.StartMonitoringLogcat(clear=False) | |
| 575 logging.info('<<< Waiting for logcat:' + str(search_re.pattern)) | |
| 576 t0 = time.time() | |
| 577 try: | |
| 578 while True: | |
| 579 # Note this will block for upto the timeout _per log line_, so we need | |
| 580 # to calculate the overall timeout remaining since t0. | |
| 581 time_remaining = t0 + self._logcat.timeout - time.time() | |
| 582 if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat) | |
| 583 self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining) | |
| 584 line = self._logcat.match.group(1) | |
| 585 search_match = search_re.search(line) | |
| 586 if search_match: | |
| 587 return search_match | |
| 588 logging.info('<<< Skipped Logcat Line:' + str(line)) | |
| 589 except pexpect.TIMEOUT: | |
| 590 raise pexpect.TIMEOUT( | |
| 591 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv ' | |
| 592 'to debug)' % | |
| 593 (self._logcat.timeout, search_re.pattern)) | |
| 594 | |
| 595 def StartRecordingLogcat(self, clear=True, filters=['*:v']): | |
| 596 """Starts recording logcat output to eventually be saved as a string. | |
| 597 | |
| 598 This call should come before some series of tests are run, with either | |
| 599 StopRecordingLogcat or SearchLogcatRecord following the tests. | |
| 600 | |
| 601 Args: | |
| 602 clear: True if existing log output should be cleared. | |
| 603 filters: A list of logcat filters to be used. | |
| 604 """ | |
| 605 if clear: | |
| 606 self._adb.SendCommand('logcat -c') | |
| 607 logcat_command = 'adb logcat -v threadtime %s' % ' '.join(filters) | |
| 608 self.logcat_process = subprocess.Popen(logcat_command, shell=True, | |
| 609 stdout=subprocess.PIPE) | |
| 610 | |
| 611 def StopRecordingLogcat(self): | |
| 612 """Stops an existing logcat recording subprocess and returns output. | |
| 613 | |
| 614 Returns: | |
| 615 The logcat output as a string or an empty string if logcat was not | |
| 616 being recorded at the time. | |
| 617 """ | |
| 618 if not self.logcat_process: | |
| 619 return '' | |
| 620 # Cannot evaluate directly as 0 is a possible value. | |
| 621 # Better to read the self.logcat_process.stdout before killing it, | |
| 622 # Otherwise the communicate may return incomplete output due to pipe break. | |
| 623 if self.logcat_process.poll() == None: | |
| 624 self.logcat_process.kill() | |
| 625 (output, _) = self.logcat_process.communicate() | |
| 626 self.logcat_process = None | |
| 627 return output | |
| 628 | |
| 629 def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None, | |
| 630 log_level=None, component=None): | |
| 631 """Searches the specified logcat output and returns results. | |
| 632 | |
| 633 This method searches through the logcat output specified by record for a | |
| 634 certain message, narrowing results by matching them against any other | |
| 635 specified criteria. It returns all matching lines as described below. | |
| 636 | |
| 637 Args: | |
| 638 record: A string generated by Start/StopRecordingLogcat to search. | |
| 639 message: An output string to search for. | |
| 640 thread_id: The thread id that is the origin of the message. | |
| 641 proc_id: The process that is the origin of the message. | |
| 642 log_level: The log level of the message. | |
| 643 component: The name of the component that would create the message. | |
| 644 | |
| 645 Returns: | |
| 646 A list of dictionaries represeting matching entries, each containing keys | |
| 647 thread_id, proc_id, log_level, component, and message. | |
| 648 """ | |
| 649 if thread_id: | |
| 650 thread_id = str(thread_id) | |
| 651 if proc_id: | |
| 652 proc_id = str(proc_id) | |
| 653 results = [] | |
| 654 reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$', | |
| 655 re.MULTILINE) | |
| 656 log_list = reg.findall(record) | |
| 657 for (tid, pid, log_lev, comp, msg) in log_list: | |
| 658 if ((not thread_id or thread_id == tid) and | |
| 659 (not proc_id or proc_id == pid) and | |
| 660 (not log_level or log_level == log_lev) and | |
| 661 (not component or component == comp) and msg.find(message) > -1): | |
| 662 match = dict({'thread_id': tid, 'proc_id': pid, | |
| 663 'log_level': log_lev, 'component': comp, | |
| 664 'message': msg}) | |
| 665 results.append(match) | |
| 666 return results | |
| 667 | |
| 668 def ExtractPid(self, process_name): | |
| 669 """Extracts Process Ids for a given process name from Android Shell. | |
| 670 | |
| 671 Args: | |
| 672 process_name: name of the process on the device. | |
| 673 | |
| 674 Returns: | |
| 675 List of all the process ids (as strings) that match the given name. | |
| 676 """ | |
| 677 pids = [] | |
| 678 for line in self.RunShellCommand('ps'): | |
| 679 data = line.split() | |
| 680 try: | |
| 681 if process_name in data[-1]: # name is in the last column | |
| 682 pids.append(data[1]) # PID is in the second column | |
| 683 except IndexError: | |
| 684 pass | |
| 685 return pids | |
| 686 | |
| 687 def GetIoStats(self): | |
| 688 """Gets cumulative disk IO stats since boot (for all processes). | |
| 689 | |
| 690 Returns: | |
| 691 Dict of {num_reads, num_writes, read_ms, write_ms} or None if there | |
| 692 was an error. | |
| 693 """ | |
| 694 # Field definitions. | |
| 695 # http://www.kernel.org/doc/Documentation/iostats.txt | |
| 696 device = 2 | |
| 697 num_reads_issued_idx = 3 | |
| 698 num_reads_merged_idx = 4 | |
| 699 num_sectors_read_idx = 5 | |
| 700 ms_spent_reading_idx = 6 | |
| 701 num_writes_completed_idx = 7 | |
| 702 num_writes_merged_idx = 8 | |
| 703 num_sectors_written_idx = 9 | |
| 704 ms_spent_writing_idx = 10 | |
| 705 num_ios_in_progress_idx = 11 | |
| 706 ms_spent_doing_io_idx = 12 | |
| 707 ms_spent_doing_io_weighted_idx = 13 | |
| 708 | |
| 709 for line in self.RunShellCommand('cat /proc/diskstats'): | |
| 710 fields = line.split() | |
| 711 if fields[device] == 'mmcblk0': | |
| 712 return { | |
| 713 'num_reads': int(fields[num_reads_issued_idx]), | |
| 714 'num_writes': int(fields[num_writes_completed_idx]), | |
| 715 'read_ms': int(fields[ms_spent_reading_idx]), | |
| 716 'write_ms': int(fields[ms_spent_writing_idx]), | |
| 717 } | |
| 718 logging.warning('Could not find disk IO stats.') | |
| 719 return None | |
| 720 | |
| 721 def GetMemoryUsage(self, package): | |
| 722 """Returns the memory usage for all processes whose name contains |pacakge|. | |
| 723 | |
| 724 Args: | |
| 725 name: A string holding process name to lookup pid list for. | |
| 726 | |
| 727 Returns: | |
| 728 Dict of {metric:usage_kb}, summed over all pids associated with |name|. | |
| 729 The metric keys retruned are: Size, Rss, Pss, Shared_Clean, Shared_Dirty, | |
| 730 Private_Clean, Private_Dirty, Referenced, Swap, KernelPageSize, | |
| 731 MMUPageSize. | |
| 732 """ | |
| 733 usage_dict = collections.defaultdict(int) | |
| 734 pid_list = self.ExtractPid(package) | |
| 735 # We used to use the showmap command, but it is currently broken on | |
| 736 # stingray so it's easier to just parse /proc/<pid>/smaps directly. | |
| 737 memory_stat_re = re.compile('^(?P<key>\w+):\s+(?P<value>\d+) kB$') | |
| 738 for pid in pid_list: | |
| 739 for line in self.RunShellCommand('cat /proc/%s/smaps' % pid, | |
| 740 log_result=False): | |
| 741 match = re.match(memory_stat_re, line) | |
| 742 if match: usage_dict[match.group('key')] += int(match.group('value')) | |
| 743 if not usage_dict or not any(usage_dict.values()): | |
| 744 # Presumably the process died between ps and showmap. | |
| 745 logging.warning('Could not find memory usage for pid ' + str(pid)) | |
| 746 return usage_dict | |
| 747 | |
| 748 def UnlockDevice(self): | |
| 749 """Unlocks the screen of the device.""" | |
| 750 # Make sure a menu button event will actually unlock the screen. | |
| 751 if IsRunningAsBuildbot(): | |
| 752 assert self.RunShellCommand('getprop ro.test_harness')[0].strip() == '1' | |
| 753 # The following keyevent unlocks the screen if locked. | |
| 754 self.SendKeyEvent(KEYCODE_MENU) | |
| 755 # If the screen wasn't locked the previous command will bring up the menu, | |
| 756 # which this will dismiss. Otherwise this shouldn't change anything. | |
| 757 self.SendKeyEvent(KEYCODE_BACK) | |
| 758 | |
| 759 | |
| 760 def main(argv): | |
| 761 option_parser = optparse.OptionParser() | |
| 762 option_parser.add_option('-w', '--wait_for_pm', action='store_true', | |
| 763 default=False, dest='wait_for_pm', | |
| 764 help='Waits for Device Package Manager to become available') | |
| 765 option_parser.add_option('--enable_asserts', dest='set_asserts', | |
| 766 action='store_true', default=None, | |
| 767 help='Sets the dalvik.vm.enableassertions property to "all"') | |
| 768 option_parser.add_option('--disable_asserts', dest='set_asserts', | |
| 769 action='store_false', default=None, | |
| 770 help='Removes the dalvik.vm.enableassertions property') | |
| 771 options, args = option_parser.parse_args(argv) | |
| 772 | |
| 773 commands = AndroidCommands(wait_for_pm=options.wait_for_pm) | |
| 774 if options.set_asserts != None: | |
| 775 if commands.SetJavaAssertsEnabled(options.set_asserts): | |
| 776 commands.Reboot(full_reboot=False) | |
| 777 | |
| 778 | |
| 779 if __name__ == '__main__': | |
| 780 main(sys.argv) | |
| OLD | NEW |