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 |