Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2017 The Chromium Authors. All rights reserved. | 2 # Copyright 2017 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 # This is intended to be a very trimmed down, single-file, hackable, and easy | 6 # This is intended to be a very trimmed down, single-file, hackable, and easy |
| 7 # to understand version of Telemetry. It's able to run simple user stories on | 7 # to understand version of Telemetry. It's able to run simple user stories on |
| 8 # Android, grab traces, and extract metrics from them. May be useful to | 8 # Android, grab traces, and extract metrics from them. May be useful to |
| 9 # diagnose issues with Chrome, reproduce regressions or prototype new user | 9 # diagnose issues with Chrome, reproduce regressions or prototype new user |
| 10 # stories. | 10 # stories. |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 23 import posixpath | 23 import posixpath |
| 24 import re | 24 import re |
| 25 import socket | 25 import socket |
| 26 import subprocess | 26 import subprocess |
| 27 import tempfile | 27 import tempfile |
| 28 import time | 28 import time |
| 29 import websocket # pylint: disable=import-error | 29 import websocket # pylint: disable=import-error |
| 30 from xml.etree import ElementTree as element_tree | 30 from xml.etree import ElementTree as element_tree |
| 31 | 31 |
| 32 | 32 |
| 33 KEYCODE_BACK = 4 | |
| 34 | |
| 33 # Parse rectangle bounds given as: '[left,top][right,bottom]'. | 35 # Parse rectangle bounds given as: '[left,top][right,bottom]'. |
| 34 RE_BOUNDS = re.compile( | 36 RE_BOUNDS = re.compile( |
| 35 r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]') | 37 r'\[(?P<left>\d+),(?P<top>\d+)\]\[(?P<right>\d+),(?P<bottom>\d+)\]') |
| 36 | 38 |
| 37 # TODO: Maybe replace with a true on-device temp file. | 39 # TODO: Maybe replace with a true on-device temp file. |
| 38 UI_DUMP_TEMP = '/data/local/tmp/tm_ui_dump.xml' | 40 UI_DUMP_TEMP = '/data/local/tmp/tm_ui_dump.xml' |
| 39 | 41 |
| 40 | 42 |
| 41 def RetryOn(exc_type=(), returns_falsy=False, retries=5): | 43 def RetryOn(exc_type=(), returns_falsy=False, retries=5): |
| 42 """Decorator to retry a function in case of errors or falsy values. | 44 """Decorator to retry a function in case of errors or falsy values. |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 135 row = line.split(None, 8) | 137 row = line.split(None, 8) |
| 136 try: | 138 try: |
| 137 pid = int(row[1]) | 139 pid = int(row[1]) |
| 138 process_name = row[-1] | 140 process_name = row[-1] |
| 139 except StandardError: | 141 except StandardError: |
| 140 continue | 142 continue |
| 141 result[process_name].append(pid) | 143 result[process_name].append(pid) |
| 142 return result | 144 return result |
| 143 | 145 |
| 144 @RetryOn(AdbCommandError) | 146 @RetryOn(AdbCommandError) |
| 145 def GetUiDump(self): | 147 def GetUiScreenDump(self): |
| 146 """Return the root XML node with screen captured from the device.""" | 148 """Return the root XML node with screen captured from the device.""" |
| 147 self.RunShellCommand('rm', '-f', UI_DUMP_TEMP) | 149 self.RunShellCommand('rm', '-f', UI_DUMP_TEMP) |
| 148 output = self.RunShellCommand('uiautomator', 'dump', UI_DUMP_TEMP).strip() | 150 output = self.RunShellCommand('uiautomator', 'dump', UI_DUMP_TEMP).strip() |
| 149 | 151 |
| 150 if output.startswith('ERROR:'): | 152 if output.startswith('ERROR:'): |
| 151 # uiautomator may fail if device is not in idle state, e.g. animations | 153 # uiautomator may fail if device is not in idle state, e.g. animations |
| 152 # or video playing. Retry if that's the case. | 154 # or video playing. Retry if that's the case. |
| 153 raise AdbCommandError(output) | 155 raise AdbCommandError(output) |
| 154 | 156 |
| 155 with tempfile.NamedTemporaryFile(suffix='.xml') as f: | 157 with tempfile.NamedTemporaryFile(suffix='.xml') as f: |
| 156 f.close() | 158 f.close() |
| 157 self.RunCommand('pull', UI_DUMP_TEMP, f.name) | 159 self.RunCommand('pull', UI_DUMP_TEMP, f.name) |
| 158 return element_tree.parse(f.name) | 160 return element_tree.parse(f.name) |
| 159 | 161 |
| 160 @RetryOn(LookupError) | 162 @RetryOn(LookupError) |
| 161 def FindUiNode(self, attr_values): | 163 def FindUiElement(self, attr_values): |
| 162 """Find a UI node on screen capture, retrying if not yet visible.""" | 164 """Find a UI element on screen capture, retrying if not yet visible.""" |
| 163 root = self.GetUiDump() | 165 root = self.GetUiScreenDump() |
| 164 for node in root.iter(): | 166 for node in root.iter(): |
| 165 if all(node.get(k) == v for k, v in attr_values): | 167 if all(node.get(k) == v for k, v in attr_values): |
| 166 return node | 168 return node |
| 167 raise LookupError('Specified UI node not found') | 169 raise LookupError('Specified UI element not found') |
| 168 | 170 |
| 169 def TapUiNode(self, *args, **kwargs): | 171 def TapUiElement(self, *args, **kwargs): |
| 170 node = self.FindUiNode(*args, **kwargs) | 172 node = self.FindUiElement(*args, **kwargs) |
| 171 m = RE_BOUNDS.match(node.get('bounds')) | 173 m = RE_BOUNDS.match(node.get('bounds')) |
| 172 left, top, right, bottom = (int(v) for v in m.groups()) | 174 left, top, right, bottom = (int(v) for v in m.groups()) |
| 173 x, y = (left + right) / 2, (top + bottom) / 2 | 175 x, y = (left + right) / 2, (top + bottom) / 2 |
| 174 self.RunShellCommand('input', 'tap', str(x), str(y)) | 176 self.RunShellCommand('input', 'tap', str(x), str(y)) |
| 175 | 177 |
| 176 | 178 |
| 179 def _UserAction(f): | |
| 180 @functools.wraps(f) | |
| 181 def Wrapper(self, *args, **kwargs): | |
| 182 repeat = kwargs.setdefault('repeat', 1) | |
| 183 action_delay = kwargs.setdefault('action_delay', None) | |
| 184 for _ in xrange(repeat): | |
| 185 f(self, *args, **kwargs) | |
| 186 self.Idle(action_delay) | |
| 187 return Wrapper | |
| 188 | |
| 189 | |
| 190 class AndroidActions(object): | |
| 191 def __init__(self, device, user_action_delay=1): | |
| 192 self.device = device | |
| 193 self.user_action_delay = user_action_delay | |
| 194 | |
| 195 def Idle(self, duration=None): | |
| 196 if duration is None: | |
| 197 duration = self.user_action_delay | |
| 198 if duration: | |
| 199 time.sleep(duration) | |
| 200 | |
| 201 @_UserAction | |
| 202 def GoBack(self, **kwargs): | |
|
nednguyen
2017/08/17 10:28:58
why do you add these **kwargs to just delete them
| |
| 203 del kwargs | |
| 204 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_BACK)) | |
| 205 | |
| 206 @_UserAction | |
| 207 def StartActivity( | |
| 208 self, data_uri, action='android.intent.action.VIEW', **kwargs): | |
| 209 del kwargs | |
| 210 self.device.RunShellCommand('am', 'start', '-a', action, '-d', data_uri) | |
| 211 | |
| 212 @_UserAction | |
| 213 def TapUiElement(self, attr_values, **kwargs): | |
| 214 del kwargs | |
| 215 self.device.TapUiElement(attr_values) | |
| 216 | |
| 217 @_UserAction | |
| 218 def SwipeUp(self, **kwargs): | |
| 219 del kwargs | |
| 220 # Hardcoded values for 480x854 screen size; should work reasonably on | |
| 221 # other screen sizes. | |
| 222 # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)] | |
| 223 self.device.RunShellCommand( | |
| 224 'input', 'swipe', '240', '568', '240', '284', '400') | |
| 225 | |
| 226 | |
| 177 class DevToolsWebSocket(object): | 227 class DevToolsWebSocket(object): |
| 178 def __init__(self, url): | 228 def __init__(self, url): |
| 179 self._url = url | 229 self._url = url |
| 180 self._socket = None | 230 self._socket = None |
| 181 self._cmdid = 0 | 231 self._cmdid = 0 |
| 182 | 232 |
| 183 def __enter__(self): | 233 def __enter__(self): |
| 184 self.Open() | 234 self.Open() |
| 185 return self | 235 return self |
| 186 | 236 |
| (...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 387 def Uninstall(self): | 437 def Uninstall(self): |
| 388 # System Chrome app cannot be (un)installed, so we enable/disable instead. | 438 # System Chrome app cannot be (un)installed, so we enable/disable instead. |
| 389 logging.warning('Disabling %s', self.PACKAGE_NAME) | 439 logging.warning('Disabling %s', self.PACKAGE_NAME) |
| 390 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) | 440 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) |
| 391 | 441 |
| 392 | 442 |
| 393 class UserStory(object): | 443 class UserStory(object): |
| 394 def __init__(self, browser): | 444 def __init__(self, browser): |
| 395 self.device = browser.device | 445 self.device = browser.device |
| 396 self.browser = browser | 446 self.browser = browser |
| 447 self.actions = AndroidActions(self.device) | |
| 397 | 448 |
| 398 def GetExtraStoryApps(self): | 449 def GetExtraStoryApps(self): |
| 399 """Sequence of AndroidApp's, other than the browser, used in the story.""" | 450 """Sequence of AndroidApp's, other than the browser, used in the story.""" |
| 400 return () | 451 return () |
| 401 | 452 |
| 402 def EnsureExtraStoryAppsClosed(self): | 453 def EnsureExtraStoryAppsClosed(self): |
| 403 running_processes = self.device.ProcessStatus() | 454 running_processes = self.device.ProcessStatus() |
| 404 for app in self.GetExtraStoryApps(): | 455 for app in self.GetExtraStoryApps(): |
| 405 if app.PACKAGE_NAME in running_processes: | 456 if app.PACKAGE_NAME in running_processes: |
| 406 app.ForceStop() | 457 app.ForceStop() |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 439 if event['ph'] == 'v': | 490 if event['ph'] == 'v': |
| 440 # Extract any metrics you may need from the trace. | 491 # Extract any metrics you may need from the trace. |
| 441 value = event['args']['dumps']['allocators'][ | 492 value = event['args']['dumps']['allocators'][ |
| 442 'java_heap/allocated_objects']['attrs']['size'] | 493 'java_heap/allocated_objects']['attrs']['size'] |
| 443 assert value['units'] == 'bytes' | 494 assert value['units'] == 'bytes' |
| 444 processes[event['pid']]['java_heap'] = int(value['value'], 16) | 495 processes[event['pid']]['java_heap'] = int(value['value'], 16) |
| 445 elif event['ph'] == 'M' and event['name'] == 'process_name': | 496 elif event['ph'] == 'M' and event['name'] == 'process_name': |
| 446 processes[event['pid']]['name'] = event['args']['name'] | 497 processes[event['pid']]['name'] = event['args']['name'] |
| 447 | 498 |
| 448 return processes.values() | 499 return processes.values() |
| OLD | NEW |