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' | |
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 | |
204 | 175 |
205 def GetLogTimestamp(log_line, year): | 176 def GetLogTimestamp(log_line, year): |
206 """Returns the timestamp of the given |log_line| in the given year.""" | 177 """Returns the timestamp of the given |log_line| in the given year.""" |
207 try: | 178 try: |
208 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), | 179 return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]), |
209 '%Y-%m-%d %H:%M:%S.%f') | 180 '%Y-%m-%d %H:%M:%S.%f') |
210 except (ValueError, IndexError): | 181 except (ValueError, IndexError): |
211 logging.critical('Error reading timestamp from ' + log_line) | 182 logging.critical('Error reading timestamp from ' + log_line) |
212 return None | 183 return None |
213 | 184 |
214 | 185 |
215 class AndroidCommands(object): | 186 class AndroidCommands(object): |
216 """Helper class for communicating with Android device via adb. | 187 """Helper class for communicating with Android device via adb. |
217 | 188 |
218 Args: | 189 Args: |
219 device: If given, adb commands are only send to the device of this ID. | 190 device: If given, adb commands are only send to the device of this ID. |
220 Otherwise commands are sent to all attached devices. | 191 Otherwise commands are sent to all attached devices. |
221 """ | 192 """ |
222 | 193 |
223 def __init__(self, device=None): | 194 def __init__(self, device=None): |
224 self._adb = adb_interface.AdbInterface() | 195 self._adb = adb_interface.AdbInterface() |
225 if device: | 196 if device: |
226 self._adb.SetTargetSerial(device) | 197 self._adb.SetTargetSerial(device) |
227 self._logcat = None | 198 self._logcat = None |
228 self._original_governor = None | 199 self._original_governor = None |
229 self._pushed_files = [] | 200 self._pushed_files = [] |
230 self._device_utc_offset = self.RunShellCommand('date +%z')[0] | 201 self._device_utc_offset = self.RunShellCommand('date +%z')[0] |
202 self._md5sum_path = '%s/out/Debug/md5sum' % (CHROME_SRC) | |
203 if not os.path.exists(self._md5sum_path): | |
204 self._md5sum_path = '%s/out/Release/md5sum' % (chrome_src_path) | |
205 if not os.path.exists(self._md5sum_path): | |
206 print >>sys.stderr, 'Please build md5sum (\'make md5sum\')' | |
Isaac (away)
2012/08/30 06:50:57
nit: you can remove the backslash escape chars if
Philippe
2012/08/30 08:22:12
Done.
| |
207 sys.exit(1) | |
Isaac (away)
2012/08/30 06:50:57
Most functions in android_commands do not require
Philippe
2012/08/30 08:22:12
Good point. I moved this check to PushIfNeeded().
| |
208 # Push the md5sum binary to the device if needed. | |
209 if not self.FileExistsOnDevice(MD5SUM_DEVICE_PATH): | |
210 command = 'push %s %s' % (self._md5sum_path, MD5SUM_DEVICE_PATH) | |
211 logging.info(command) | |
212 assert self._adb.SendCommand(command) | |
Isaac (away)
2012/08/30 06:50:57
Is this assert being used correctly? it will succ
Philippe
2012/08/30 08:22:12
Indeed. I moved the code we had in PushIfNeeded()
| |
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 md5. |
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 |
561 self._pushed_files.append(device_path) | 538 self._pushed_files.append(device_path) |
562 | 539 |
563 # If the path contents are the same, there's nothing to do. | 540 hashes_on_device = _ComputeFileListHash( |
564 local_contents = ListHostPathContents(local_path) | 541 self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path)) |
565 device_contents = self.ListPathContents(device_path) | 542 assert os.path.exists(local_path), 'Local path not found %s' % local_path |
566 # Only compare the size and timestamp if only copying a file because | 543 hashes_on_host = _ComputeFileListHash( |
567 # the filename on device can be renamed. | 544 cmd_helper.GetCmdOutput( |
568 if os.path.isfile(local_path): | 545 '%s_host_bin %s' % (self._md5sum_path, local_path), shell=True)) |
569 assert len(local_contents) == 1 | 546 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 | 547 return |
576 | 548 |
577 # They don't match, so remove everything first and then create it. | 549 # They don't match, so remove everything first and then create it. |
578 if os.path.isdir(local_path): | 550 if os.path.isdir(local_path): |
579 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) | 551 self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60) |
580 self.RunShellCommand('mkdir -p %s' % device_path) | 552 self.RunShellCommand('mkdir -p %s' % device_path) |
581 | 553 |
582 # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of | 554 # 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. | 555 # 60 seconds which isn't sufficient for a lot of users of this method. |
584 push_command = 'push %s %s' % (local_path, device_path) | 556 push_command = 'push %s %s' % (local_path, device_path) |
(...skipping 402 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
987 continue | 959 continue |
988 # Column 0 is the executable name | 960 # Column 0 is the executable name |
989 # Column 1 is the pid | 961 # Column 1 is the pid |
990 # Column 8 is the Inode in use | 962 # Column 8 is the Inode in use |
991 if process_results[8] == socket_name: | 963 if process_results[8] == socket_name: |
992 pids.append((int(process_results[1]), process_results[0])) | 964 pids.append((int(process_results[1]), process_results[0])) |
993 break | 965 break |
994 logging.info('PidsUsingDevicePort: %s', pids) | 966 logging.info('PidsUsingDevicePort: %s', pids) |
995 return pids | 967 return pids |
996 | 968 |
969 def FileExistsOnDevice(self, file_name): | |
970 """Checks whether the given (regular) file exists on the device. | |
971 | |
972 Args: | |
973 file_name: Full path of file to check. | |
974 | |
975 Returns: | |
976 True if the file exists, False otherwise. | |
977 """ | |
978 assert '"' not in file_name, 'file_name cannot contain double quotes' | |
979 status = self._adb.SendShellCommand( | |
980 '\'test -f "%s"; echo $?\'' % (file_name)) | |
Philippe
2012/08/29 08:36:45
Do we still need a comment here? I think it's much
Isaac (away)
2012/08/30 06:50:57
this is fine, thanks
Philippe
2012/08/30 08:22:12
Done.
| |
981 return int(status) == 0 | |
982 | |
997 def RunMonkey(self, package_name, category=None, throttle=100, seed=None, | 983 def RunMonkey(self, package_name, category=None, throttle=100, seed=None, |
998 event_count=10000, verbosity=1, extra_args=''): | 984 event_count=10000, verbosity=1, extra_args=''): |
999 """Runs monkey test for a given package. | 985 """Runs monkey test for a given package. |
1000 | 986 |
1001 Args: | 987 Args: |
1002 package_name: Allowed package. | 988 package_name: Allowed package. |
1003 category: A list of allowed categories. | 989 category: A list of allowed categories. |
1004 throttle: Delay between events (ms). | 990 throttle: Delay between events (ms). |
1005 seed: Seed value for pseduo-random generator. Same seed value | 991 seed: Seed value for pseduo-random generator. Same seed value |
1006 generates the same sequence of events. Seed is randomized by | 992 generates the same sequence of events. Seed is randomized by |
(...skipping 11 matching lines...) Expand all Loading... | |
1018 cmd = ['monkey', | 1004 cmd = ['monkey', |
1019 '-p %s' % package_name, | 1005 '-p %s' % package_name, |
1020 ' '.join(['-c %s' % c for c in category]), | 1006 ' '.join(['-c %s' % c for c in category]), |
1021 '--throttle %d' % throttle, | 1007 '--throttle %d' % throttle, |
1022 '-s %d' % seed, | 1008 '-s %d' % seed, |
1023 '-v ' * verbosity, | 1009 '-v ' * verbosity, |
1024 extra_args, | 1010 extra_args, |
1025 '%s' % event_count] | 1011 '%s' % event_count] |
1026 return self.RunShellCommand(' '.join(cmd), | 1012 return self.RunShellCommand(' '.join(cmd), |
1027 timeout_time=event_count*throttle*1.5) | 1013 timeout_time=event_count*throttle*1.5) |
OLD | NEW |