Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(20)

Side by Side Diff: experimental/telemetry_mini/telemetry_mini.py

Issue 2996123002: [telemetry_mini] Add AndroidActions class (Closed)
Patch Set: explain **kwargs Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « experimental/telemetry_mini/android_go_stories.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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 """Decorator to add repeat, and action_delay options to user action methods.
181
182 Note: The values (or their defaults) supplied for these extra options will
183 also be passed down to the decorated method. It's thus advisable to collect
184 them in a catch-all **kwargs, even if just to discard them immediately.
185
186 This is a workaround for https://github.com/PyCQA/pylint/issues/258 in which
187 decorators confuse pylint and trigger spurious 'unexpected-keyword-arg'
188 warnings on method calls that use the extra options.
189 """
190 @functools.wraps(f)
191 def Wrapper(self, *args, **kwargs):
192 repeat = kwargs.setdefault('repeat', 1)
193 action_delay = kwargs.setdefault('action_delay', None)
194 for _ in xrange(repeat):
195 f(self, *args, **kwargs)
196 self.Idle(action_delay)
197 return Wrapper
198
199
200 class AndroidActions(object):
201 def __init__(self, device, user_action_delay=1):
202 self.device = device
203 self.user_action_delay = user_action_delay
204
205 def Idle(self, duration=None):
206 if duration is None:
207 duration = self.user_action_delay
208 if duration:
209 time.sleep(duration)
210
211 @_UserAction
212 def GoBack(self, **kwargs):
213 del kwargs
214 self.device.RunShellCommand('input', 'keyevent', str(KEYCODE_BACK))
215
216 @_UserAction
217 def StartActivity(
218 self, data_uri, action='android.intent.action.VIEW', **kwargs):
219 del kwargs
220 self.device.RunShellCommand('am', 'start', '-a', action, '-d', data_uri)
221
222 @_UserAction
223 def TapUiElement(self, attr_values, **kwargs):
224 del kwargs
225 self.device.TapUiElement(attr_values)
226
227 @_UserAction
228 def SwipeUp(self, **kwargs):
229 del kwargs
230 # Hardcoded values for 480x854 screen size; should work reasonably on
231 # other screen sizes.
232 # Command args: swipe <x1> <y1> <x2> <y2> [duration(ms)]
233 self.device.RunShellCommand(
234 'input', 'swipe', '240', '568', '240', '284', '400')
235
236
177 class DevToolsWebSocket(object): 237 class DevToolsWebSocket(object):
178 def __init__(self, url): 238 def __init__(self, url):
179 self._url = url 239 self._url = url
180 self._socket = None 240 self._socket = None
181 self._cmdid = 0 241 self._cmdid = 0
182 242
183 def __enter__(self): 243 def __enter__(self):
184 self.Open() 244 self.Open()
185 return self 245 return self
186 246
(...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after
387 def Uninstall(self): 447 def Uninstall(self):
388 # System Chrome app cannot be (un)installed, so we enable/disable instead. 448 # System Chrome app cannot be (un)installed, so we enable/disable instead.
389 logging.warning('Disabling %s', self.PACKAGE_NAME) 449 logging.warning('Disabling %s', self.PACKAGE_NAME)
390 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME) 450 self.device.RunShellCommand('pm', 'disable', self.PACKAGE_NAME)
391 451
392 452
393 class UserStory(object): 453 class UserStory(object):
394 def __init__(self, browser): 454 def __init__(self, browser):
395 self.device = browser.device 455 self.device = browser.device
396 self.browser = browser 456 self.browser = browser
457 self.actions = AndroidActions(self.device)
397 458
398 def GetExtraStoryApps(self): 459 def GetExtraStoryApps(self):
399 """Sequence of AndroidApp's, other than the browser, used in the story.""" 460 """Sequence of AndroidApp's, other than the browser, used in the story."""
400 return () 461 return ()
401 462
402 def EnsureExtraStoryAppsClosed(self): 463 def EnsureExtraStoryAppsClosed(self):
403 running_processes = self.device.ProcessStatus() 464 running_processes = self.device.ProcessStatus()
404 for app in self.GetExtraStoryApps(): 465 for app in self.GetExtraStoryApps():
405 if app.PACKAGE_NAME in running_processes: 466 if app.PACKAGE_NAME in running_processes:
406 app.ForceStop() 467 app.ForceStop()
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
439 if event['ph'] == 'v': 500 if event['ph'] == 'v':
440 # Extract any metrics you may need from the trace. 501 # Extract any metrics you may need from the trace.
441 value = event['args']['dumps']['allocators'][ 502 value = event['args']['dumps']['allocators'][
442 'java_heap/allocated_objects']['attrs']['size'] 503 'java_heap/allocated_objects']['attrs']['size']
443 assert value['units'] == 'bytes' 504 assert value['units'] == 'bytes'
444 processes[event['pid']]['java_heap'] = int(value['value'], 16) 505 processes[event['pid']]['java_heap'] = int(value['value'], 16)
445 elif event['ph'] == 'M' and event['name'] == 'process_name': 506 elif event['ph'] == 'M' and event['name'] == 'process_name':
446 processes[event['pid']]['name'] = event['args']['name'] 507 processes[event['pid']]['name'] = event['args']['name']
447 508
448 return processes.values() 509 return processes.values()
OLDNEW
« no previous file with comments | « experimental/telemetry_mini/android_go_stories.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698