OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Provides an interface to communicate with the device via the adb command. | 5 """Provides an interface to communicate with the device via the adb command. |
6 | 6 |
7 Assumes adb binary is currently on system path. | 7 Assumes adb binary is currently on system path. |
8 """ | 8 """ |
9 | 9 |
10 import collections | 10 import collections |
11 import datetime | 11 import datetime |
12 import logging | 12 import logging |
13 import os | 13 import os |
14 import random | 14 import random |
15 import re | 15 import re |
16 import shlex | 16 import shlex |
17 import subprocess | 17 import subprocess |
18 import sys | 18 import sys |
19 import tempfile | 19 import tempfile |
20 import time | 20 import time |
21 | 21 |
22 import pexpect | 22 import pexpect |
23 import io_stats_parser | 23 import io_stats_parser |
24 | 24 |
25 # adb_interface.py is under ../../../third_party/android_testrunner/ | 25 CHROME_SRC = os.path.join( |
26 sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', | 26 os.path.abspath(os.path.dirname(__file__)), '..', '..', '..') |
27 '..', '..', 'third_party', 'android_testrunner')) | 27 |
| 28 sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner')) |
28 import adb_interface | 29 import adb_interface |
| 30 |
29 import cmd_helper | 31 import cmd_helper |
30 import errors # is under ../../../third_party/android_testrunner/errors.py | 32 import errors # is under ../../../third_party/android_testrunner/errors.py |
31 | 33 |
32 | 34 |
33 # Pattern to search for the next whole line of pexpect output and capture it | 35 # Pattern to search for the next whole line of pexpect output and capture it |
34 # into a match group. We can't use ^ and $ for line start end with pexpect, | 36 # into a match group. We can't use ^ and $ for line start end with pexpect, |
35 # see http://www.noah.org/python/pexpect/#doc for explanation why. | 37 # see http://www.noah.org/python/pexpect/#doc for explanation why. |
36 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') | 38 PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r') |
37 | 39 |
38 # Set the adb shell prompt to be a unique marker that will [hopefully] not | 40 # Set the adb shell prompt to be a unique marker that will [hopefully] not |
(...skipping 20 matching lines...) Expand all Loading... |
59 | 61 |
60 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). | 62 # Keycode "enum" suitable for passing to AndroidCommands.SendKey(). |
61 KEYCODE_HOME = 3 | 63 KEYCODE_HOME = 3 |
62 KEYCODE_BACK = 4 | 64 KEYCODE_BACK = 4 |
63 KEYCODE_DPAD_UP = 19 | 65 KEYCODE_DPAD_UP = 19 |
64 KEYCODE_DPAD_DOWN = 20 | 66 KEYCODE_DPAD_DOWN = 20 |
65 KEYCODE_DPAD_RIGHT = 22 | 67 KEYCODE_DPAD_RIGHT = 22 |
66 KEYCODE_ENTER = 66 | 68 KEYCODE_ENTER = 66 |
67 KEYCODE_MENU = 82 | 69 KEYCODE_MENU = 82 |
68 | 70 |
| 71 MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum_bin' |
69 | 72 |
70 def GetEmulators(): | 73 def GetEmulators(): |
71 """Returns a list of emulators. Does not filter by status (e.g. offline). | 74 """Returns a list of emulators. Does not filter by status (e.g. offline). |
72 | 75 |
73 Both devices starting with 'emulator' will be returned in below output: | 76 Both devices starting with 'emulator' will be returned in below output: |
74 | 77 |
75 * daemon not running. starting it now on port 5037 * | 78 * daemon not running. starting it now on port 5037 * |
76 * daemon started successfully * | 79 * daemon started successfully * |
77 List of devices attached | 80 List of devices attached |
78 027c10494100b4d7 device | 81 027c10494100b4d7 device |
(...skipping 27 matching lines...) Expand all Loading... |
106 emulator-5554 offline | 109 emulator-5554 offline |
107 """ | 110 """ |
108 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) | 111 re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE) |
109 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) | 112 devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices'])) |
110 preferred_device = os.environ.get('ANDROID_SERIAL') | 113 preferred_device = os.environ.get('ANDROID_SERIAL') |
111 if preferred_device in devices: | 114 if preferred_device in devices: |
112 devices.remove(preferred_device) | 115 devices.remove(preferred_device) |
113 devices.insert(0, preferred_device) | 116 devices.insert(0, preferred_device) |
114 return devices | 117 return devices |
115 | 118 |
116 | |
117 def _GetHostFileInfo(file_name): | |
118 """Returns a tuple containing size and modified UTC time for file_name.""" | |
119 # The time accuracy on device is only to minute level, remove the second and | |
120 # microsecond from host results. | |
121 utc_time = datetime.datetime.utcfromtimestamp(os.path.getmtime(file_name)) | |
122 time_delta = datetime.timedelta(seconds=utc_time.second, | |
123 microseconds=utc_time.microsecond) | |
124 return os.path.getsize(file_name), utc_time - time_delta | |
125 | |
126 | |
127 def ListHostPathContents(path): | |
128 """Lists files in all subdirectories of |path|. | |
129 | |
130 Args: | |
131 path: The path to list. | |
132 | |
133 Returns: | |
134 A dict of {"name": (size, lastmod), ...}. | |
135 """ | |
136 if os.path.isfile(path): | |
137 return {os.path.basename(path): _GetHostFileInfo(path)} | |
138 ret = {} | |
139 for root, dirs, files in os.walk(path): | |
140 for d in dirs: | |
141 if d.startswith('.'): | |
142 dirs.remove(d) # Prune the dir for subsequent iterations. | |
143 for f in files: | |
144 if f.startswith('.'): | |
145 continue | |
146 full_file_name = os.path.join(root, f) | |
147 file_name = os.path.relpath(full_file_name, path) | |
148 ret[file_name] = _GetHostFileInfo(full_file_name) | |
149 return ret | |
150 | |
151 | |
152 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): | 119 def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None): |
153 """Gets a list of files from `ls` command output. | 120 """Gets a list of files from `ls` command output. |
154 | 121 |
155 Python's os.walk isn't used because it doesn't work over adb shell. | 122 Python's os.walk isn't used because it doesn't work over adb shell. |
156 | 123 |
157 Args: | 124 Args: |
158 path: The path to list. | 125 path: The path to list. |
159 ls_output: A list of lines returned by an `ls -lR` command. | 126 ls_output: A list of lines returned by an `ls -lR` command. |
160 re_file: A compiled regular expression which parses a line into named groups | 127 re_file: A compiled regular expression which parses a line into named groups |
161 consisting of at minimum "filename", "date", "time", "size" and | 128 consisting of at minimum "filename", "date", "time", "size" and |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
194 utc_offset = file_match.group('timezone') | 161 utc_offset = file_match.group('timezone') |
195 if isinstance(utc_offset, str) and len(utc_offset) == 5: | 162 if isinstance(utc_offset, str) and len(utc_offset) == 5: |
196 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), | 163 utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]), |
197 minutes=int(utc_offset[3:5])) | 164 minutes=int(utc_offset[3:5])) |
198 if utc_offset[0:1] == '-': | 165 if utc_offset[0:1] == '-': |
199 utc_delta = -utc_delta | 166 utc_delta = -utc_delta |
200 lastmod -= utc_delta | 167 lastmod -= utc_delta |
201 files[filename] = (int(file_match.group('size')), lastmod) | 168 files[filename] = (int(file_match.group('size')), lastmod) |
202 return files | 169 return files |
203 | 170 |
| 171 def _ComputeFileListHash(md5sum_output): |
| 172 """Returns a list of MD5 strings from the provided md5sum output.""" |
| 173 return [line.split(' ')[0] for line in md5sum_output] |
| 174 |
| 175 def _HasAdbPushSucceeded(command_output): |
| 176 """Returns whether adb push has succeeded from the provided output.""" |
| 177 if not command_output: |
| 178 return False |
| 179 # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" |
| 180 # Errors look like this: "failed to copy ... " |
| 181 if not re.search('^[0-9]', command_output.splitlines()[-1]): |
| 182 logging.critical('PUSH FAILED: ' + command_output) |
| 183 return False |
| 184 return True |
204 | 185 |
205 def GetLogTimestamp(log_line, year): | 186 def GetLogTimestamp(log_line, year): |
206 """Returns the timestamp of the given |log_line| in the given year.""" | 187 """Returns the timestamp of the given |log_line| in the given year.""" |
207 try: | 188 try: |
208 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), | 189 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), |
209 '%Y-%m-%d %H:%M:%S.%f') | 190 '%Y-%m-%d %H:%M:%S.%f') |
210 except (ValueError, IndexError): | 191 except (ValueError, IndexError): |
211 logging.critical('Error reading timestamp from ' + log_line) | 192 logging.critical('Error reading timestamp from ' + log_line) |
212 return None | 193 return None |
213 | 194 |
214 | 195 |
215 class AndroidCommands(object): | 196 class AndroidCommands(object): |
216 """Helper class for communicating with Android device via adb. | 197 """Helper class for communicating with Android device via adb. |
217 | 198 |
218 Args: | 199 Args: |
219 device: If given, adb commands are only send to the device of this ID. | 200 device: If given, adb commands are only send to the device of this ID. |
220 Otherwise commands are sent to all attached devices. | 201 Otherwise commands are sent to all attached devices. |
221 """ | 202 """ |
222 | 203 |
223 def __init__(self, device=None): | 204 def __init__(self, device=None): |
224 self._adb = adb_interface.AdbInterface() | 205 self._adb = adb_interface.AdbInterface() |
225 if device: | 206 if device: |
226 self._adb.SetTargetSerial(device) | 207 self._adb.SetTargetSerial(device) |
227 self._logcat = None | 208 self._logcat = None |
228 self._original_governor = None | 209 self._original_governor = None |
229 self._pushed_files = [] | 210 self._pushed_files = [] |
230 self._device_utc_offset = self.RunShellCommand('date +%z')[0] | 211 self._device_utc_offset = self.RunShellCommand('date +%z')[0] |
| 212 self._md5sum_path = '' |
231 | 213 |
232 def Adb(self): | 214 def Adb(self): |
233 """Returns our AdbInterface to avoid us wrapping all its methods.""" | 215 """Returns our AdbInterface to avoid us wrapping all its methods.""" |
234 return self._adb | 216 return self._adb |
235 | 217 |
236 def IsRootEnabled(self): | 218 def IsRootEnabled(self): |
237 """Checks if root is enabled on the device.""" | 219 """Checks if root is enabled on the device.""" |
238 root_test_output = self.RunShellCommand('ls /root') or [''] | 220 root_test_output = self.RunShellCommand('ls /root') or [''] |
239 return not 'Permission denied' in root_test_output[0] | 221 return not 'Permission denied' in root_test_output[0] |
240 | 222 |
(...skipping 16 matching lines...) Expand all Loading... |
257 try: | 239 try: |
258 self._adb.WaitForDevicePm() | 240 self._adb.WaitForDevicePm() |
259 return # Success | 241 return # Success |
260 except errors.WaitForResponseTimedOutError as e: | 242 except errors.WaitForResponseTimedOutError as e: |
261 last_err = e | 243 last_err = e |
262 logging.warning('Restarting and retrying after timeout: %s', e) | 244 logging.warning('Restarting and retrying after timeout: %s', e) |
263 retries -= 1 | 245 retries -= 1 |
264 self.RestartShell() | 246 self.RestartShell() |
265 raise last_err # Only reached after max retries, re-raise the last error. | 247 raise last_err # Only reached after max retries, re-raise the last error. |
266 | 248 |
267 def SynchronizeDateTime(self): | |
268 """Synchronize date/time between host and device.""" | |
269 self._adb.SendShellCommand('date -u %f' % time.time()) | |
270 | |
271 def RestartShell(self): | 249 def RestartShell(self): |
272 """Restarts the shell on the device. Does not block for it to return.""" | 250 """Restarts the shell on the device. Does not block for it to return.""" |
273 self.RunShellCommand('stop') | 251 self.RunShellCommand('stop') |
274 self.RunShellCommand('start') | 252 self.RunShellCommand('start') |
275 | 253 |
276 def Reboot(self, full_reboot=True): | 254 def Reboot(self, full_reboot=True): |
277 """Reboots the device and waits for the package manager to return. | 255 """Reboots the device and waits for the package manager to return. |
278 | 256 |
279 Args: | 257 Args: |
280 full_reboot: Whether to fully reboot the device or just restart the shell. | 258 full_reboot: Whether to fully reboot the device or just restart the shell. |
(...skipping 264 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
545 | 523 |
546 Args: | 524 Args: |
547 keycode: Numeric keycode to send (see "enum" at top of file). | 525 keycode: Numeric keycode to send (see "enum" at top of file). |
548 """ | 526 """ |
549 self.RunShellCommand('input keyevent %d' % keycode) | 527 self.RunShellCommand('input keyevent %d' % keycode) |
550 | 528 |
551 def PushIfNeeded(self, local_path, device_path): | 529 def PushIfNeeded(self, local_path, device_path): |
552 """Pushes |local_path| to |device_path|. | 530 """Pushes |local_path| to |device_path|. |
553 | 531 |
554 Works for files and directories. This method skips copying any paths in | 532 Works for files and directories. This method skips copying any paths in |
555 |test_data_paths| that already exist on the device with the same timestamp | 533 |test_data_paths| that already exist on the device with the same hash. |
556 and size. | |
557 | 534 |
558 All pushed files can be removed by calling RemovePushedFiles(). | 535 All pushed files can be removed by calling RemovePushedFiles(). |
559 """ | 536 """ |
560 assert os.path.exists(local_path), 'Local path not found %s' % local_path | 537 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
| 538 |
| 539 if not self._md5sum_path: |
| 540 default_build_type = os.environ.get('BUILD_TYPE', 'Debug') |
| 541 md5sum_path = '%s/out/%s/md5sum_bin' % (CHROME_SRC, default_build_type) |
| 542 if not os.path.exists(md5sum_path): |
| 543 md5sum_path = '%s/out/Release/md5sum_bin' % (CHROME_SRC) |
| 544 if not os.path.exists(md5sum_path): |
| 545 print >>sys.stderr, 'Please build md5sum.' |
| 546 sys.exit(1) |
| 547 if not self.FileExistsOnDevice(MD5SUM_DEVICE_PATH): |
| 548 command = 'push %s %s' % (md5sum_path, MD5SUM_DEVICE_PATH) |
| 549 assert _HasAdbPushSucceeded(self._adb.SendCommand(command)) |
| 550 self._md5sum_path = md5sum_path |
| 551 |
561 self._pushed_files.append(device_path) | 552 self._pushed_files.append(device_path) |
562 | 553 hashes_on_device = _ComputeFileListHash( |
563 # If the path contents are the same, there's nothing to do. | 554 self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path)) |
564 local_contents = ListHostPathContents(local_path) | 555 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
565 device_contents = self.ListPathContents(device_path) | 556 hashes_on_host = _ComputeFileListHash( |
566 # Only compare the size and timestamp if only copying a file because | 557 subprocess.Popen( |
567 # the filename on device can be renamed. | 558 '%s_host %s' % (self._md5sum_path, local_path), |
568 if os.path.isfile(local_path): | 559 stdout=subprocess.PIPE, shell=True).stdout) |
569 assert len(local_contents) == 1 | 560 if hashes_on_device == hashes_on_host: |
570 is_equal = local_contents.values() == device_contents.values() | |
571 else: | |
572 is_equal = local_contents == device_contents | |
573 if is_equal: | |
574 logging.info('%s is up-to-date. Skipping file push.', device_path) | |
575 return | 561 return |
576 | 562 |
577 # They don't match, so remove everything first and then create it. | 563 # They don't match, so remove everything first and then create it. |
578 if os.path.isdir(local_path): | 564 if os.path.isdir(local_path): |
579 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) | 565 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) |
580 self.RunShellCommand('mkdir -p %s' % device_path) | 566 self.RunShellCommand('mkdir -p %s' % device_path) |
581 | 567 |
582 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of | 568 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of |
583 # 60 seconds which isn't sufficient for a lot of users of this method. | 569 # 60 seconds which isn't sufficient for a lot of users of this method. |
584 push_command = 'push %s %s' % (local_path, device_path) | 570 push_command = 'push %s %s' % (local_path, device_path) |
585 logging.info('>>> $' + push_command) | 571 logging.info('>>> $' + push_command) |
586 output = self._adb.SendCommand(push_command, timeout_time=30*60) | 572 output = self._adb.SendCommand(push_command, timeout_time=30*60) |
587 assert output | 573 assert _HasAdbPushSucceeded(output) |
588 # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)" | 574 |
589 # Errors look like this: "failed to copy ... " | |
590 if not re.search('^[0-9]', output.splitlines()[-1]): | |
591 logging.critical('PUSH FAILED: ' + output) | |
592 | 575 |
593 def GetFileContents(self, filename, log_result=False): | 576 def GetFileContents(self, filename, log_result=False): |
594 """Gets contents from the file specified by |filename|.""" | 577 """Gets contents from the file specified by |filename|.""" |
595 return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' + | 578 return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' + |
596 filename + '"; fi', log_result=log_result) | 579 filename + '"; fi', log_result=log_result) |
597 | 580 |
598 def SetFileContents(self, filename, contents): | 581 def SetFileContents(self, filename, contents): |
599 """Writes |contents| to the file specified by |filename|.""" | 582 """Writes |contents| to the file specified by |filename|.""" |
600 with tempfile.NamedTemporaryFile() as f: | 583 with tempfile.NamedTemporaryFile() as f: |
601 f.write(contents) | 584 f.write(contents) |
(...skipping 385 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
987 continue | 970 continue |
988 # Column 0 is the executable name | 971 # Column 0 is the executable name |
989 # Column 1 is the pid | 972 # Column 1 is the pid |
990 # Column 8 is the Inode in use | 973 # Column 8 is the Inode in use |
991 if process_results[8] == socket_name: | 974 if process_results[8] == socket_name: |
992 pids.append((int(process_results[1]), process_results[0])) | 975 pids.append((int(process_results[1]), process_results[0])) |
993 break | 976 break |
994 logging.info('PidsUsingDevicePort: %s', pids) | 977 logging.info('PidsUsingDevicePort: %s', pids) |
995 return pids | 978 return pids |
996 | 979 |
| 980 def FileExistsOnDevice(self, file_name): |
| 981 """Checks whether the given (regular) file exists on the device. |
| 982 |
| 983 Args: |
| 984 file_name: Full path of file to check. |
| 985 |
| 986 Returns: |
| 987 True if the file exists, False otherwise. |
| 988 """ |
| 989 assert '"' not in file_name, 'file_name cannot contain double quotes' |
| 990 status = self._adb.SendShellCommand( |
| 991 '\'test -f "%s"; echo $?\'' % (file_name)) |
| 992 return int(status) == 0 |
| 993 |
997 def RunMonkey(self, package_name, category=None, throttle=100, seed=None, | 994 def RunMonkey(self, package_name, category=None, throttle=100, seed=None, |
998 event_count=10000, verbosity=1, extra_args=''): | 995 event_count=10000, verbosity=1, extra_args=''): |
999 """Runs monkey test for a given package. | 996 """Runs monkey test for a given package. |
1000 | 997 |
1001 Args: | 998 Args: |
1002 package_name: Allowed package. | 999 package_name: Allowed package. |
1003 category: A list of allowed categories. | 1000 category: A list of allowed categories. |
1004 throttle: Delay between events (ms). | 1001 throttle: Delay between events (ms). |
1005 seed: Seed value for pseduo-random generator. Same seed value | 1002 seed: Seed value for pseduo-random generator. Same seed value |
1006 generates the same sequence of events. Seed is randomized by | 1003 generates the same sequence of events. Seed is randomized by |
(...skipping 13 matching lines...) Expand all Loading... |
1020 ' '.join(['-c %s' % c for c in category]), | 1017 ' '.join(['-c %s' % c for c in category]), |
1021 '--throttle %d' % throttle, | 1018 '--throttle %d' % throttle, |
1022 '-s %d' % seed, | 1019 '-s %d' % seed, |
1023 '-v ' * verbosity, | 1020 '-v ' * verbosity, |
1024 '--monitor-native-crashes', | 1021 '--monitor-native-crashes', |
1025 '--kill-process-after-error', | 1022 '--kill-process-after-error', |
1026 extra_args, | 1023 extra_args, |
1027 '%d' % event_count] | 1024 '%d' % event_count] |
1028 return self.RunShellCommand(' '.join(cmd), | 1025 return self.RunShellCommand(' '.join(cmd), |
1029 timeout_time=event_count*throttle*1.5) | 1026 timeout_time=event_count*throttle*1.5) |
OLD | NEW |