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